mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f73e5025b | ||
|
|
69f0b4cb75 | ||
|
|
2a96a72151 | ||
|
|
30c2cfbfa0 | ||
|
|
e8c3a77ba6 | ||
|
|
e15894c2f5 | ||
|
|
566fb861b0 | ||
|
|
80873a3227 | ||
|
|
809c63ada9 | ||
|
|
f0bf56f6c8 | ||
|
|
e4214ef871 | ||
|
|
88e6939347 | ||
|
|
dc73aabbd3 | ||
|
|
1dd41b0b9b | ||
|
|
328437ced2 | ||
|
|
f79bd9f2f9 | ||
|
|
84bc0434fd | ||
|
|
96045d0e17 | ||
|
|
129c608108 | ||
|
|
d73aa95684 | ||
|
|
fe9e9d2527 | ||
|
|
e5a52f3db6 | ||
|
|
84bb527e5e | ||
|
|
bd7bf5c712 | ||
|
|
b6d3106ee9 | ||
|
|
d7bc7d5d2e | ||
|
|
8eb3d58658 | ||
|
|
8c85467bcd | ||
|
|
f59f41cc3e | ||
|
|
678c90cbda | ||
|
|
7cee60596c | ||
|
|
979d24d375 | ||
|
|
cdf912db9b | ||
|
|
51867d840b | ||
|
|
51137fa66f | ||
|
|
a394928e83 | ||
|
|
c7edc70ccb | ||
|
|
2aa2c9419c | ||
|
|
5b9728dc1d | ||
|
|
8c73a32631 | ||
|
|
459c214164 | ||
|
|
96235ed242 | ||
|
|
45ce3a70c9 | ||
|
|
c101efb961 | ||
|
|
0d47ad6cc7 | ||
|
|
9a338b79d5 | ||
|
|
0087226de1 | ||
|
|
8276359359 | ||
|
|
53214e822e | ||
|
|
20c24f2722 | ||
|
|
9b88f6c008 | ||
|
|
c1bab2fb63 | ||
|
|
e6c23839e3 | ||
|
|
909448bad1 | ||
|
|
565b6eb11c | ||
|
|
bb69fcf908 | ||
|
|
7e66e7fafc | ||
|
|
007644ddfc | ||
|
|
153bacf3d0 | ||
|
|
3a5698011b | ||
|
|
95c37e5e8f | ||
|
|
633c08ee25 | ||
|
|
54e6f4fec9 | ||
|
|
ef2db12e54 | ||
|
|
b4027560cc | ||
|
|
6eb4e89199 | ||
|
|
b30def95bc | ||
|
|
423c286071 | ||
|
|
ddbbaab650 | ||
|
|
58827652f5 | ||
|
|
763f8765c9 | ||
|
|
75f83c70dc | ||
|
|
d84aaab2af | ||
|
|
69a4eb113c | ||
|
|
8c5a898309 | ||
|
|
667a0ef770 | ||
|
|
8e97596fc0 | ||
|
|
3c7259f1cb | ||
|
|
17759294fb | ||
|
|
d4925669e8 | ||
|
|
971e7a3d1b | ||
|
|
86418abd05 | ||
|
|
9331609afd | ||
|
|
9093d701e4 | ||
|
|
3ad51b7408 | ||
|
|
ebc0cb487a | ||
|
|
2771eeb3d1 | ||
|
|
4ca31490ae | ||
|
|
94ab15bd96 | ||
|
|
686cc31e49 | ||
|
|
6e1976d1e2 | ||
|
|
0408a42153 | ||
|
|
ce2e90c476 | ||
|
|
79eec6e595 | ||
|
|
c6812ecc99 | ||
|
|
208cf732ac | ||
|
|
cbd46126cd | ||
|
|
ee040806ae | ||
|
|
13d16c3f14 | ||
|
|
29cd40bfbe | ||
|
|
27249697bd | ||
|
|
3ae80fc123 | ||
|
|
7f5a66cc63 | ||
|
|
a9710ca5ec | ||
|
|
ec0239f985 | ||
|
|
a4ceae10e5 | ||
|
|
0409df0ee4 | ||
|
|
b3cc01e9e0 | ||
|
|
e7c3c4657d | ||
|
|
530254cfd5 | ||
|
|
c5ad9309cd | ||
|
|
beaca19d07 | ||
|
|
16bfbb4b62 | ||
|
|
3af18f2408 | ||
|
|
9d08501242 | ||
|
|
d8b8e0ef89 | ||
|
|
1626f76e3a | ||
|
|
d597b9e67f | ||
|
|
e1dbcd0e7e | ||
|
|
70b33a3ae4 | ||
|
|
4834515b4c | ||
|
|
fa0bf87307 | ||
|
|
4b9fe9d6a0 | ||
|
|
a674c4257d | ||
|
|
992d1c13dc | ||
|
|
7024345088 | ||
|
|
9560f84395 | ||
|
|
9f9ab3ce09 | ||
|
|
4ac29fcfe8 | ||
|
|
174a3069fe | ||
|
|
fc6b77e32b | ||
|
|
6cfb89336f | ||
|
|
635409442c | ||
|
|
2ffd2db995 | ||
|
|
431c2292cb | ||
|
|
1a4a1a1f1c | ||
|
|
fe1b5ac4c6 | ||
|
|
0e9253f698 | ||
|
|
11f9b935b4 | ||
|
|
a16896e74d | ||
|
|
814599d6ea | ||
|
|
8f2dcac0f9 | ||
|
|
240659afc9 | ||
|
|
b9059e2a52 | ||
|
|
a1688f7cdd | ||
|
|
2c378bfca2 | ||
|
|
aec495b161 | ||
|
|
486bc1a194 | ||
|
|
18bcb91d09 | ||
|
|
5c4d73f555 | ||
|
|
27e8450789 | ||
|
|
56733f2445 | ||
|
|
089b00fcd6 | ||
|
|
a709fe70ba | ||
|
|
f26e4b4859 | ||
|
|
18a382167b | ||
|
|
b4be0a3e6b | ||
|
|
596ac08ad4 | ||
|
|
9c6ad0027a | ||
|
|
27ce374f35 | ||
|
|
7e6f311b62 | ||
|
|
dd77fdff9e | ||
|
|
0b7adf4888 | ||
|
|
9deee8a384 | ||
|
|
81791381a5 | ||
|
|
9806639c1b | ||
|
|
5e9a3c4b99 | ||
|
|
12f73f4dee | ||
|
|
9d27dfebe5 | ||
|
|
7bebd4c845 | ||
|
|
43d2e84b32 | ||
|
|
ee1cb395a2 | ||
|
|
4e221d8867 | ||
|
|
3aab7b74a3 | ||
|
|
92651b8bc7 | ||
|
|
08b53c34f2 | ||
|
|
8d6a311936 | ||
|
|
79b8b92b66 | ||
|
|
fa8a07481a | ||
|
|
f79d6ef0fb | ||
|
|
bb25541202 | ||
|
|
3530723bd5 | ||
|
|
f2ffaacc6c | ||
|
|
14e95fe4df | ||
|
|
f69b12a1f9 | ||
|
|
e4e527eaeb | ||
|
|
c4c8f6123b | ||
|
|
bdb71242a7 | ||
|
|
a3a36fcef1 | ||
|
|
adbcb1bb88 | ||
|
|
37504f9e97 | ||
|
|
1722bcd7a3 | ||
|
|
a770873da5 | ||
|
|
a4c3385956 | ||
|
|
9fe491e7cc | ||
|
|
42213948b7 | ||
|
|
1014348e5b | ||
|
|
dcecd66e49 | ||
|
|
4509e51f66 | ||
|
|
90841369bb | ||
|
|
c3e41c8a60 | ||
|
|
81ffbf653e | ||
|
|
b46e30c8fa | ||
|
|
3619698744 | ||
|
|
81ee6b8146 | ||
|
|
c9b27fdc64 | ||
|
|
6b50c07ed7 | ||
|
|
d2670e180c | ||
|
|
580e1b4ed0 | ||
|
|
e84e487307 | ||
|
|
d0a54c95a6 | ||
|
|
18ac10f399 | ||
|
|
263e070b13 | ||
|
|
8cf9ceac6e | ||
|
|
32005ce23f | ||
|
|
111a397811 | ||
|
|
454032368a | ||
|
|
5509cd4b9a | ||
|
|
346c9f9803 | ||
|
|
0a738df3d0 | ||
|
|
b67da1c2c4 | ||
|
|
e700586588 | ||
|
|
9bb7c1c6f1 | ||
|
|
7f70968531 | ||
|
|
9ae35e46db | ||
|
|
3d667fe1c6 | ||
|
|
62e07ce571 | ||
|
|
38cd587369 | ||
|
|
77c9801e4c | ||
|
|
d3f1e101ce | ||
|
|
10a2d2743e | ||
|
|
4d26f42f25 | ||
|
|
e8f3d9f601 | ||
|
|
6f3af6f135 | ||
|
|
3e08836d4e | ||
|
|
efea7cdc9e | ||
|
|
c6232e617f | ||
|
|
f360716b8e | ||
|
|
55bf6b0457 | ||
|
|
3067b37298 | ||
|
|
a19a343feb | ||
|
|
edb5ca5422 | ||
|
|
2eb5f10eee | ||
|
|
7ce269d8be | ||
|
|
42554bba7a | ||
|
|
be77832f72 | ||
|
|
f32d29ca34 | ||
|
|
bb3652f9c4 | ||
|
|
a6272bff4f | ||
|
|
29f8cf110b | ||
|
|
05ccbd6b16 | ||
|
|
608f2a921e | ||
|
|
7bd40f7030 | ||
|
|
0075a5f994 | ||
|
|
06414288f8 | ||
|
|
ac9b32408c | ||
|
|
4ad1e4237c | ||
|
|
2f0ebd04d0 | ||
|
|
40264f28d1 | ||
|
|
95eaef7f8d | ||
|
|
7a1fa68c94 | ||
|
|
1d86557ed4 | ||
|
|
d013809645 | ||
|
|
32b860bf0b | ||
|
|
02ea189fcb | ||
|
|
d38b647fc3 | ||
|
|
0eb31cc8ea | ||
|
|
7aef916e70 | ||
|
|
fc84ef78fd | ||
|
|
7c31425114 | ||
|
|
c88ad2201d | ||
|
|
e936510754 | ||
|
|
825e549b49 | ||
|
|
b8b0ffe34b | ||
|
|
e2018d3e4c | ||
|
|
c3dbc9682e | ||
|
|
54195f0a85 | ||
|
|
600795ca48 | ||
|
|
3b2b04b10d | ||
|
|
57ddbcaa36 | ||
|
|
1185793e4d | ||
|
|
d8a5989ba1 | ||
|
|
7a8ba85071 | ||
|
|
378b886947 | ||
|
|
167fa3b979 | ||
|
|
7ddcf94b57 | ||
|
|
f71c406180 | ||
|
|
c1a94acb92 | ||
|
|
1264d4f3b9 | ||
|
|
3ab87a18e3 | ||
|
|
2a68a9cc9b | ||
|
|
15f86a85d4 | ||
|
|
16952cece6 | ||
|
|
4c71092e7f | ||
|
|
d2558a40d8 | ||
|
|
c918c5ffc9 | ||
|
|
54ecddc46e | ||
|
|
f2e655bb05 | ||
|
|
5ee3e7310e | ||
|
|
8bdb30bca5 | ||
|
|
bc06295f97 | ||
|
|
522e9f851e | ||
|
|
28e798ff28 | ||
|
|
ca4576e155 | ||
|
|
90309143fd | ||
|
|
8f4fcb4364 | ||
|
|
facd2c15b9 | ||
|
|
756cadd0e6 | ||
|
|
842a561f94 | ||
|
|
9346b1e245 | ||
|
|
b93b430554 | ||
|
|
a90dd779d5 | ||
|
|
dafec6bd6b | ||
|
|
b27e23e7fb | ||
|
|
6fff1f15fb | ||
|
|
85bfc6041e | ||
|
|
eb1a92e96a | ||
|
|
8c7a94b476 | ||
|
|
f683d33769 | ||
|
|
0c84cbf08f | ||
|
|
358f629285 | ||
|
|
af77bc500e | ||
|
|
224e199931 | ||
|
|
cfcb28aa58 | ||
|
|
469a2be823 | ||
|
|
61e0a3b380 | ||
|
|
5b092f905e | ||
|
|
01715f4879 | ||
|
|
6acef8abad | ||
|
|
44743b70ca | ||
|
|
fcd637a2fa | ||
|
|
f5e08acf04 | ||
|
|
04c4ee1b80 | ||
|
|
d87ee4bd84 | ||
|
|
04758edba0 | ||
|
|
796feea25c | ||
|
|
52c47a214e | ||
|
|
92450f634c | ||
|
|
fec92c752f | ||
|
|
5d6bbfb4b5 | ||
|
|
3c22bbc3d1 | ||
|
|
5e5edede6f | ||
|
|
a2a4a05f02 | ||
|
|
4f0f5acb88 | ||
|
|
f00ef92e86 | ||
|
|
38f1872d68 | ||
|
|
c314138d89 | ||
|
|
b174082623 | ||
|
|
38dcf3dcae | ||
|
|
a872500fb1 | ||
|
|
5a9d8b922c | ||
|
|
9ee09fdbcf | ||
|
|
8a9e2d2c5c | ||
|
|
1f955889c6 | ||
|
|
a565179cb2 | ||
|
|
42307f6e56 | ||
|
|
bfd3119a28 | ||
|
|
596688f701 | ||
|
|
eef3c3c766 | ||
|
|
c1f81ef855 | ||
|
|
30ac43e131 | ||
|
|
67cb87c932 | ||
|
|
f044966725 | ||
|
|
6606755d99 | ||
|
|
9afe77977d | ||
|
|
12aef60d93 | ||
|
|
3479c95a6d | ||
|
|
a0b3e3f76c | ||
|
|
107f64d85a | ||
|
|
03327f59c1 | ||
|
|
98d2e52e64 | ||
|
|
7608f62869 | ||
|
|
03c3b99f3d | ||
|
|
af08a0b681 | ||
|
|
9ea473dc68 | ||
|
|
44ee47c34d | ||
|
|
18e4055588 | ||
|
|
ba819d4074 | ||
|
|
b9eaf86711 | ||
|
|
9f92bb94b1 | ||
|
|
51cb20c75d | ||
|
|
fe1243f9b8 | ||
|
|
b81c7140a8 | ||
|
|
b5fb6274dd | ||
|
|
4c363b2658 | ||
|
|
881da1c79d | ||
|
|
e8301077f6 | ||
|
|
d59c6092b3 | ||
|
|
09dfd45cb2 | ||
|
|
008e808134 | ||
|
|
370084845d | ||
|
|
de73afef15 | ||
|
|
d981144bc4 | ||
|
|
6e4151bd14 | ||
|
|
939fd69e6d | ||
|
|
05520f31ec | ||
|
|
2786fbfc0f | ||
|
|
8ec998044b | ||
|
|
29c914069e | ||
|
|
24033598f9 | ||
|
|
94584e9faa | ||
|
|
953cf38ffd | ||
|
|
0b16c5eab2 | ||
|
|
58de142232 | ||
|
|
a88e0c0362 | ||
|
|
7cb5f86937 | ||
|
|
e45e4ec239 | ||
|
|
14308236c6 | ||
|
|
d4254cc60f | ||
|
|
4fcb3fcd07 | ||
|
|
a23bc4c0e3 | ||
|
|
8f44b40cff | ||
|
|
0e9dfad793 | ||
|
|
02e30db67f | ||
|
|
d840758123 | ||
|
|
b6fc419e53 | ||
|
|
aed379e4c4 | ||
|
|
15faebf71b | ||
|
|
4d837e5308 | ||
|
|
901899cb76 | ||
|
|
ebbd4dabe9 | ||
|
|
db77c431f5 | ||
|
|
f813b6c1a6 | ||
|
|
e1799a2f0a | ||
|
|
d770d629d3 | ||
|
|
a42988106d | ||
|
|
24f6631fb5 | ||
|
|
fceff7e673 | ||
|
|
3641e131a7 | ||
|
|
c6570f6d35 | ||
|
|
ea156482be | ||
|
|
c33862bde9 | ||
|
|
e5cb8855f2 | ||
|
|
2fc629fc96 | ||
|
|
2924d1147b | ||
|
|
a5d8fdda5d | ||
|
|
27837a09e2 | ||
|
|
b0d80a7e70 | ||
|
|
c52c0a3337 | ||
|
|
4682ae4fc9 | ||
|
|
1e9ffe6d1f | ||
|
|
b8522d800f | ||
|
|
8854bc0f38 | ||
|
|
acbb19a2bb | ||
|
|
6e04c28f0b | ||
|
|
670a276f9a | ||
|
|
d5aa9bc70e | ||
|
|
b1ca01cd47 | ||
|
|
1f59e00f54 | ||
|
|
4609d156a7 | ||
|
|
1a170be522 | ||
|
|
65022b5894 | ||
|
|
04e989736b | ||
|
|
303449f2b6 | ||
|
|
27394ea7ef | ||
|
|
18448389b6 | ||
|
|
5e08eb5753 | ||
|
|
a8ac538ca5 | ||
|
|
74a37b8549 | ||
|
|
1796678a01 | ||
|
|
d8173e6cf3 | ||
|
|
e539f42dc0 | ||
|
|
15f782ff23 | ||
|
|
59ce52b138 | ||
|
|
626d0942b8 | ||
|
|
20d5fb5ca9 | ||
|
|
1f9cc8451b | ||
|
|
36db9a8ee2 | ||
|
|
0318f5a0f8 | ||
|
|
d89e16ea5f | ||
|
|
cc42a2040f | ||
|
|
62da9c574d | ||
|
|
3f71655748 | ||
|
|
c973b672b7 | ||
|
|
2d588c0be4 | ||
|
|
40a65c2000 | ||
|
|
7c604213ff | ||
|
|
f46fdda4ff | ||
|
|
5c2bd77ffe | ||
|
|
ad2fb13ef0 | ||
|
|
78f167ea24 | ||
|
|
15a7ffb300 | ||
|
|
e8754aba72 | ||
|
|
1ef1d4028f | ||
|
|
29ccc7a730 | ||
|
|
7a8c82ae0a | ||
|
|
65185e5e31 | ||
|
|
dc66370fd0 | ||
|
|
7d31b1b2c8 | ||
|
|
0c8be55d1d | ||
|
|
dd54f2474b | ||
|
|
2788273d79 | ||
|
|
242508153b | ||
|
|
6ef04fa9d7 | ||
|
|
8e735531a5 | ||
|
|
4862f3e9a3 | ||
|
|
984cf8e3e8 | ||
|
|
16cec93af9 | ||
|
|
8bc71edafb | ||
|
|
1c4e502a4b | ||
|
|
cbce10d373 | ||
|
|
243a3f566d | ||
|
|
1d6cdb05ec | ||
|
|
b50ce20072 | ||
|
|
9c2ed04462 | ||
|
|
ced41fd7da | ||
|
|
b1cc7fd91c | ||
|
|
cdf2809cd8 | ||
|
|
2eb7ab4254 | ||
|
|
a41ab262af | ||
|
|
f907b76023 | ||
|
|
8c9bd4c3e0 | ||
|
|
8090823b2b | ||
|
|
85679662b5 | ||
|
|
c0f72c93e2 | ||
|
|
9b25d47f6f | ||
|
|
e694123330 | ||
|
|
fa48c0cc75 | ||
|
|
540386fed5 | ||
|
|
df0ac9bb2b | ||
|
|
d0bd2ea311 | ||
|
|
3a243e5c26 | ||
|
|
38a6f9c58a | ||
|
|
1cce0da021 | ||
|
|
fda6455a85 | ||
|
|
488020df23 | ||
|
|
65b48361b3 | ||
|
|
0be8f6f192 | ||
|
|
9888ae4599 | ||
|
|
8b4198d477 | ||
|
|
9bc73d7aaf | ||
|
|
ea6e77cd6a | ||
|
|
ba1350114a | ||
|
|
4a2bfe8362 | ||
|
|
b3cb3b15d5 | ||
|
|
bdc862a935 | ||
|
|
f63a4fdf20 | ||
|
|
57c53c254a | ||
|
|
83a15d737c | ||
|
|
68ee9bd31e | ||
|
|
1c4b8b3f33 | ||
|
|
e065a64258 | ||
|
|
af5cb0e160 | ||
|
|
f79aafa6cf | ||
|
|
0b5b4b5ebd | ||
|
|
e61a4cf09c | ||
|
|
11d58a9fc4 | ||
|
|
074942d4a5 | ||
|
|
92e402b0e6 | ||
|
|
515456feaf | ||
|
|
1f3dce0f60 | ||
|
|
5b21c246f8 | ||
|
|
2be6a004e3 | ||
|
|
27e285c182 | ||
|
|
6aeb78c90d | ||
|
|
f8bc7606e8 | ||
|
|
3968a6407c | ||
|
|
6467bdf555 | ||
|
|
677922a2cf | ||
|
|
8e2e760ffc | ||
|
|
c606dd9040 | ||
|
|
ea62047af5 | ||
|
|
2990c6e903 | ||
|
|
c273cba4be | ||
|
|
bef81b60fd | ||
|
|
e314dd0647 | ||
|
|
eb87cff81e | ||
|
|
473d81eac0 | ||
|
|
bcd8febcc8 | ||
|
|
401114e5c6 | ||
|
|
49d237ada6 | ||
|
|
992eb51393 | ||
|
|
3d46f0d753 | ||
|
|
7b2b760997 | ||
|
|
8401ba0d55 | ||
|
|
9e3a1adcc7 | ||
|
|
1dee45c937 | ||
|
|
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 |
+2
-1
@@ -14,6 +14,7 @@
|
||||
"no-console": "error",
|
||||
"react/no-unescaped-entities": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
"prettier/prettier": "error"
|
||||
"prettier/prettier": "error",
|
||||
"cypress/no-assigning-return-values": "off"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
name: 'Run tests'
|
||||
description: 'Runs Cypress tests for individual spec'
|
||||
|
||||
inputs:
|
||||
username:
|
||||
required: true
|
||||
description: 'Toska registry username'
|
||||
password:
|
||||
required: true
|
||||
description: 'Toska registry password'
|
||||
spec:
|
||||
required: true
|
||||
description: 'Path to Cypress file'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
shell: bash
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ inputs.username }}
|
||||
password: ${{ inputs.password }}
|
||||
- name: 'Run services, pull rest of the needed images'
|
||||
shell: bash
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
- name: 'Run cypress'
|
||||
uses: cypress-io/github-action@v4
|
||||
with:
|
||||
browser: chrome
|
||||
spec: ${{ inputs.spec }}
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: Cypress videos
|
||||
path: ./cypress/videos/*
|
||||
- name: 'Print logs on failure'
|
||||
shell: bash
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
+66
-13
@@ -1,15 +1,68 @@
|
||||
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'
|
||||
ignore:
|
||||
- dependency-name: '*'
|
||||
update-types: ['version-update:semver-major']
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: '/updater/sis-updater-scheduler'
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: '03:00'
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'updater'
|
||||
ignore:
|
||||
- dependency-name: '*'
|
||||
update-types: ['version-update:semver-major']
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: '/updater/sis-updater-worker'
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: '03:00'
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'updater'
|
||||
ignore:
|
||||
- dependency-name: '*'
|
||||
update-types: ['version-update:semver-major']
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: '/services/backend'
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: '03:00'
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'back'
|
||||
ignore:
|
||||
- dependency-name: '*'
|
||||
update-types: ['version-update:semver-major']
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: '/services/frontend'
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: '03:00'
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'front'
|
||||
ignore:
|
||||
- dependency-name: '*'
|
||||
update-types: ['version-update:semver-major']
|
||||
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: '03:00'
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
@@ -9,10 +9,10 @@ jobs:
|
||||
name: 'Build and release backend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Login to DockerHub'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -21,9 +21,9 @@ jobs:
|
||||
# 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/}"
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
- name: 'Build and push'
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
|
||||
@@ -41,24 +41,24 @@ jobs:
|
||||
name: 'Build and release frontend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Login to DockerHub'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 'Get current time'
|
||||
id: get_current_time
|
||||
run: echo ::set-output name=NOW::"$(date --iso-8601=seconds)"
|
||||
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
|
||||
- name: 'Extract release version'
|
||||
# Github context (e.g. github.ref) only has ref formatted as /refs/tags/vX.X.X
|
||||
# This steps extracts release version from GITHUB_REF env var
|
||||
# See: https://github.community/t/how-to-get-just-the-tag-name/16241/10
|
||||
id: extract_version
|
||||
run: echo ::set-output name=VERSION::"${GITHUB_REF#refs/tags/}"
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
- name: 'Build and push'
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
|
||||
|
||||
@@ -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,162 @@
|
||||
name: 'Test and release updater'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '.github/workflows/test-and-release-updater.yaml'
|
||||
- 'updater/sis-updater-worker/**'
|
||||
- 'updater/sis-updater-scheduler/**'
|
||||
|
||||
jobs:
|
||||
build_worker:
|
||||
name: 'Build sis-updater-worker'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Build the image'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
SENTRY_ENVIRONMENT=production
|
||||
context: updater/sis-updater-worker/
|
||||
target: production
|
||||
push: false
|
||||
outputs: type=docker,dest=/tmp/worker.tar
|
||||
tags: toska/sis-updater-worker:latest
|
||||
cache-from: type=registry,ref=toska/sis-updater-worker:latest
|
||||
cache-to: type=inline
|
||||
- name: 'Store built image'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: /tmp/worker.tar
|
||||
|
||||
build_scheduler:
|
||||
name: 'Build sis-updater-scheduler'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Build the image'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
SENTRY_ENVIRONMENT=production
|
||||
context: updater/sis-updater-scheduler/
|
||||
target: production
|
||||
push: false
|
||||
outputs: type=docker,dest=/tmp/scheduler.tar
|
||||
tags: toska/sis-updater-scheduler:latest
|
||||
cache-from: type=registry,ref=toska/sis-updater-scheduler:latest
|
||||
cache-to: type=inline
|
||||
- name: 'Store built image'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: /tmp/scheduler.tar
|
||||
|
||||
test_updater:
|
||||
name: 'Test updater'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build_worker
|
||||
- build_scheduler
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
- name: 'Download built images'
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load worker image'
|
||||
run: docker load --input /tmp/artifact/worker.tar
|
||||
- name: 'Load scheduler image'
|
||||
run: docker load --input /tmp/artifact/scheduler.tar
|
||||
- name: 'Start worker'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate sis-updater-worker
|
||||
- name: 'Run hourly jobs'
|
||||
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
|
||||
env:
|
||||
SCHEDULE_IMMEDIATE: hourly
|
||||
EXIT_AFTER_IMMEDIATES: yes
|
||||
- name: 'Run daily jobs'
|
||||
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
|
||||
env:
|
||||
SCHEDULE_IMMEDIATE: daily
|
||||
EXIT_AFTER_IMMEDIATES: yes
|
||||
- name: 'Run weekly jobs'
|
||||
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
|
||||
env:
|
||||
SCHEDULE_IMMEDIATE: weekly
|
||||
EXIT_AFTER_IMMEDIATES: yes
|
||||
- name: 'Run prepurge'
|
||||
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
|
||||
env:
|
||||
SCHEDULE_IMMEDIATE: prepurge
|
||||
EXIT_AFTER_IMMEDIATES: yes
|
||||
- name: 'Run purge'
|
||||
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
|
||||
env:
|
||||
SCHEDULE_IMMEDIATE: purge
|
||||
EXIT_AFTER_IMMEDIATES: yes
|
||||
|
||||
release_updater:
|
||||
name: 'Release updater'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test_updater
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Login to DockerHub'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 'Build and push scheduler'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
cache-from: type=registry,ref=toska/sis-updater-scheduler:latest
|
||||
cache-to: type=inline
|
||||
context: updater/sis-updater-scheduler/
|
||||
push: true
|
||||
tags: toska/sis-updater-scheduler:latest
|
||||
target: production
|
||||
- name: 'Build and push worker'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
cache-from: type=registry,ref=toska/sis-updater-worker:latest
|
||||
cache-to: type=inline
|
||||
context: updater/sis-updater-worker/
|
||||
push: true
|
||||
tags: toska/sis-updater-worker:latest
|
||||
target: production
|
||||
- name: 'Create Sentry release'
|
||||
uses: getsentry/action-release@v1
|
||||
env:
|
||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: toska
|
||||
SENTRY_PROJECT: updater
|
||||
with:
|
||||
environment: production
|
||||
set_commits: 'skip'
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/static/js'
|
||||
version: ${{ github.sha }}
|
||||
- name: 'Send notification to slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment
|
||||
softa-url: https://oodikone.cs.helsinki.fi
|
||||
deployment-target: production
|
||||
softa-to-deploy: sis-updater
|
||||
@@ -6,13 +6,16 @@ on:
|
||||
- master
|
||||
paths:
|
||||
- '.github/workflows/test-oodikone.yaml'
|
||||
- '.github/actions/**'
|
||||
- 'cypress/**'
|
||||
- 'cypress.json'
|
||||
- 'cypress.config.json'
|
||||
- 'services/**'
|
||||
- '!services/backend/src/rapodiff.js'
|
||||
- 'docker-compose.ci.yml'
|
||||
- 'docker-compose.test.yml'
|
||||
- 'nginx.conf'
|
||||
- 'package*.json'
|
||||
- 'updater/sis-updater-worker/src/db/migrations/**'
|
||||
|
||||
jobs:
|
||||
# === Static tests / linting ===
|
||||
@@ -21,8 +24,8 @@ jobs:
|
||||
name: 'Lint backend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3.4.1
|
||||
with:
|
||||
node-version: 14
|
||||
- name: 'Install dependencies'
|
||||
@@ -34,8 +37,8 @@ jobs:
|
||||
name: 'Lint frontend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3.4.1
|
||||
with:
|
||||
node-version: 14
|
||||
- name: 'Install dependencies'
|
||||
@@ -50,10 +53,10 @@ jobs:
|
||||
name: 'Build backend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Build image and export it'
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
@@ -68,7 +71,7 @@ jobs:
|
||||
target: production
|
||||
outputs: type=docker,dest=/tmp/backend.tar
|
||||
- name: 'Upload image for other jobs'
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: /tmp/backend.tar
|
||||
|
||||
@@ -76,13 +79,13 @@ jobs:
|
||||
name: 'Build frontend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Get current time'
|
||||
id: get_current_time
|
||||
run: echo ::set-output name=NOW::"$(date --iso-8601=seconds)"
|
||||
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
|
||||
- name: 'Build image and export it'
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
@@ -98,7 +101,7 @@ jobs:
|
||||
target: production
|
||||
outputs: type=docker,dest=/tmp/frontend.tar
|
||||
- name: 'Upload image for other jobs'
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: /tmp/frontend.tar
|
||||
|
||||
@@ -106,7 +109,7 @@ jobs:
|
||||
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: actions/checkout@v3
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: check_migrations
|
||||
with:
|
||||
@@ -115,7 +118,7 @@ jobs:
|
||||
- 'updater/sis-updater-worker/src/db/migrations/**'
|
||||
- '.github/workflows/test-oodikone.yaml'
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
@@ -133,7 +136,7 @@ jobs:
|
||||
- build_backend
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: check_migrations
|
||||
with:
|
||||
@@ -142,13 +145,13 @@ jobs:
|
||||
- 'services/backend/src/database/migrations_kone/**'
|
||||
- '.github/workflows/test-oodikone.yaml'
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
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
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
@@ -166,7 +169,7 @@ jobs:
|
||||
- build_backend
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: check_migrations
|
||||
with:
|
||||
@@ -175,13 +178,13 @@ jobs:
|
||||
- 'services/backend/src/database/migrations_user/**'
|
||||
- '.github/workflows/test-oodikone.yaml'
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
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
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
@@ -204,39 +207,13 @@ jobs:
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- 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: 'Run services, pull rest of the needed images'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
- name: 'Run cypress'
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
browser: chrome
|
||||
spec: cypress/integration/Course_statistics.js
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: Cypress videos
|
||||
path: ./cypress/videos/*
|
||||
- name: 'Print logs on failure'
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
spec: cypress/e2e/Course_statistics.js
|
||||
|
||||
test_custom_population:
|
||||
name: 'Test custom population'
|
||||
@@ -248,39 +225,13 @@ jobs:
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- 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: 'Run services, pull rest of the needed images'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
- name: 'Run cypress'
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
browser: chrome
|
||||
spec: cypress/integration/Custom_population.js
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: Cypress videos
|
||||
path: ./cypress/videos/*
|
||||
- name: 'Print logs on failure'
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
spec: cypress/e2e/Custom_population.js
|
||||
|
||||
test_population_statistics:
|
||||
name: 'Test population statistics'
|
||||
@@ -292,39 +243,13 @@ jobs:
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- 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: 'Run services, pull rest of the needed images'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
- name: 'Run cypress'
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
browser: chrome
|
||||
spec: cypress/integration/Population_statistics.js
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: Cypress videos
|
||||
path: ./cypress/videos/*
|
||||
- name: 'Print logs on failure'
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
spec: cypress/e2e/Population_statistics.js
|
||||
|
||||
test_filters:
|
||||
name: 'Test filters'
|
||||
@@ -336,39 +261,13 @@ jobs:
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- 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: 'Run services, pull rest of the needed images'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
- name: 'Run cypress'
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
browser: chrome
|
||||
spec: cypress/integration/Filters.js
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: Cypress videos
|
||||
path: ./cypress/videos/*
|
||||
- name: 'Print logs on failure'
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
spec: cypress/e2e/Filters.js
|
||||
|
||||
test_student_statistics:
|
||||
name: 'Test student statistics'
|
||||
@@ -380,39 +279,13 @@ jobs:
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- 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: 'Run services, pull rest of the needed images'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
- name: 'Run cypress'
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
browser: chrome
|
||||
spec: cypress/integration/Student_statistics.js
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: Cypress videos
|
||||
path: ./cypress/videos/*
|
||||
- name: 'Print logs on failure'
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
spec: cypress/e2e/Student_statistics.js
|
||||
|
||||
test_studyprogramme_overview:
|
||||
name: 'Test studyprogramme overview'
|
||||
@@ -424,39 +297,13 @@ jobs:
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- 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: 'Run services, pull rest of the needed images'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
- name: 'Run cypress'
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
browser: chrome
|
||||
spec: cypress/integration/Studyprogramme_overview.js
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: Cypress videos
|
||||
path: ./cypress/videos/*
|
||||
- name: 'Print logs on failure'
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
spec: cypress/e2e/Studyprogramme_overview.js
|
||||
|
||||
test_teachers_and_users:
|
||||
name: 'Test teachers and users'
|
||||
@@ -468,41 +315,33 @@ jobs:
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- 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: 'Run services, pull rest of the needed images'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
- name: 'Run cypress'
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
browser: chrome
|
||||
spec: |
|
||||
cypress/integration/Teachers.js
|
||||
cypress/integration/Users.js
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
cypress/e2e/Teachers.js
|
||||
cypress/e2e/Users.js
|
||||
|
||||
test_faculty_statistics:
|
||||
name: 'Test faculty statistics'
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
name: Cypress videos
|
||||
path: ./cypress/videos/*
|
||||
- name: 'Print logs on failure'
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
spec: cypress/e2e/Faculty_statistics.js
|
||||
|
||||
# === Other tests ===
|
||||
|
||||
@@ -516,9 +355,9 @@ jobs:
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
@@ -526,7 +365,7 @@ jobs:
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
@@ -559,11 +398,12 @@ jobs:
|
||||
- test_student_statistics
|
||||
- test_studyprogramme_overview
|
||||
- test_teachers_and_users
|
||||
- test_faculty_statistics
|
||||
- test_service_exit_statuses
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
@@ -571,7 +411,7 @@ jobs:
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- name: 'Login to DockerHub'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -625,10 +465,11 @@ jobs:
|
||||
- test_student_statistics
|
||||
- test_studyprogramme_overview
|
||||
- test_teachers_and_users
|
||||
- test_faculty_statistics
|
||||
- test_service_exit_statuses
|
||||
if: failure()
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Send notification to slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ module.exports = {
|
||||
'*.css': files => `stylelint --fix ${files.join(' ')}`,
|
||||
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)}`,
|
||||
'.github/{workflows,actions}/*': files => `npm run actionlint ${relativeFilePaths(files)}`,
|
||||
'docker-compose*': files => {
|
||||
const composeFiles = file => {
|
||||
if (['docker-compose.ci.yml', 'docker-compose.test.yml'].some(f => file.includes(f))) {
|
||||
|
||||
+1
-1
@@ -5,5 +5,5 @@
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"jsxSingleQuote": false,
|
||||
"jsxSingleQuote": false
|
||||
}
|
||||
+5
-1
@@ -13,6 +13,10 @@
|
||||
"ignorePseudoClasses": ["global"]
|
||||
}
|
||||
],
|
||||
"prettier/prettier": true
|
||||
"prettier/prettier": true,
|
||||
"selector-class-pattern": null,
|
||||
"custom-property-pattern": null,
|
||||
"value-no-vendor-prefix": null,
|
||||
"property-no-vendor-prefix": null
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -48,6 +48,10 @@ What different CLI options do:
|
||||
|
||||
## ⌨️ 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**.
|
||||
@@ -104,7 +108,7 @@ There are three types of tests in this project: static tests with eslint/prettie
|
||||
|
||||
- Cypress
|
||||
- can be launched in interactive mode with `npm run cypress open`. `package.json` defines entrypoint `npm run cypress` so you can basically run cypress with any arguments you want
|
||||
- are defined in cypress -folder and cypress.json
|
||||
- are defined in cypress -folder and cypress.config.json
|
||||
- whole cypress test stack takes about 15 to 20 mins to run. Since tests are ran in our Github actions CI pipe, you're encouraged to take advantage of this instead of running all tests locally.
|
||||
- There are some different user types and cypress commands defined for testing. Take a look at these when debugging tests.
|
||||
- Jest
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
const { defineConfig } = require('cypress')
|
||||
|
||||
module.exports = defineConfig({
|
||||
projectId: 'c3jsph',
|
||||
defaultCommandTimeout: 30000,
|
||||
requestTimeout: 30000,
|
||||
videoCompression: false,
|
||||
viewportWidth: 1800,
|
||||
viewportHeight: 1200,
|
||||
e2e: {
|
||||
// We've imported your old cypress plugins here.
|
||||
// You may want to clean this up later by importing these.
|
||||
setupNodeEvents(on, config) {
|
||||
return require('./cypress/plugins/index.js')(on, config)
|
||||
},
|
||||
baseUrl: 'http://localhost:3000',
|
||||
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
|
||||
},
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"projectId": "c3jsph",
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"defaultCommandTimeout": 30000,
|
||||
"requestTimeout": 30000,
|
||||
"videoCompression": false
|
||||
}
|
||||
@@ -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()
|
||||
@@ -84,8 +82,8 @@ describe('Course Statistics tests', () => {
|
||||
cy.contains('Search for courses')
|
||||
cy.get("input[placeholder='Search by a course code']").type(courseCode)
|
||||
cy.contains('td', courseCode).click()
|
||||
cy.contains('Separate statistics for Spring and Fall semesters').click()
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.get('[data-cy=separateToggle]', { force: true }).click({ force: true })
|
||||
cy.contains('Syksy 2019')
|
||||
cy.contains('Syksy 2017')
|
||||
cy.get('tbody').contains('19')
|
||||
@@ -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('tbody > :nth-child(3) > :nth-child(2) .level').click()
|
||||
cy.contains('Population of course Käyttöjärjestelmät 2019-2020 (open and normal')
|
||||
cy.contains('TKT20003')
|
||||
|
||||
cy.contains('Students (127)').click()
|
||||
@@ -176,22 +166,23 @@ 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('tbody >: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.skip('Language distribution is correct', () => {
|
||||
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(':nth-child(3) > :nth-child(1) > div > .item > .level').click()
|
||||
cy.get('[data-cy=unify_radio_reqular]').click()
|
||||
cy.get('tbody > :nth-child(3) > :nth-child(2) .level').click()
|
||||
cy.contains('Population of course Käyttöjärjestelmät 2019-2020')
|
||||
cy.contains('Language distribution').click()
|
||||
cy.contains('td', 'finnish').siblings().contains('122')
|
||||
@@ -202,7 +193,6 @@ describe('Course Statistics tests', () => {
|
||||
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()
|
||||
@@ -214,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', () => {
|
||||
@@ -252,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)
|
||||
})
|
||||
})
|
||||
@@ -265,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)
|
||||
})
|
||||
})
|
||||
@@ -273,7 +275,45 @@ describe('Course Statistics tests', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('after changing time range shows same stats', () => {
|
||||
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']")
|
||||
.click()
|
||||
@@ -302,9 +342,25 @@ 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('Has right to see all the sudents, because course provider is TKT', () => {
|
||||
cy.visit('coursestatistics?courseCodes=%5B%22TKT10004%22%5D&cs_tab=0&separate=false')
|
||||
cy.get('tbody > :nth-child(3) > :nth-child(2) .level').click()
|
||||
cy.contains('Students (154)').click()
|
||||
cy.contains('015224224')
|
||||
})
|
||||
})
|
||||
|
||||
it('Some features of Course Statistics are hidden for courseStatistics-users without other rights', () => {
|
||||
it.skip('Some features of Course Statistics are hidden for courseStatistics-users without other rights', () => {
|
||||
cy.init('/coursestatistics', 'onlycoursestatistics')
|
||||
cy.get('[data-cy=navbar-courseStatistics]').click()
|
||||
cy.get('[data-cy=course-code-input]').type('TKT20003')
|
||||
@@ -314,13 +370,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 %'],
|
||||
['Total', 288, 213, 75, '73.96 %'], //Somehow cypress in CI can't find the number even if it is present in the artifact video
|
||||
['2020-2021', '5 or less students', 'NA', 'NA', 'NA'],
|
||||
['2019-2020', 164, 121, 43, '73.78 %'],
|
||||
['2018-2019', 85, 60, 25, '70.59 %'],
|
||||
['2017-2018', 39, 32, 7, '82.05 %'],
|
||||
// ['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()
|
||||
@@ -330,6 +390,7 @@ describe('Course Statistics tests', () => {
|
||||
.eq(trIndex)
|
||||
.within(() => {
|
||||
values.forEach((value, tdIndex) => {
|
||||
if (value === null) return
|
||||
cy.get('td').eq(tdIndex).contains(value)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,223 @@
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
// Change admin to regular user when feature is ready for general use
|
||||
|
||||
describe('Faculty overview', () => {
|
||||
describe('Faculty can be selected', () => {
|
||||
it('Faculties are listed and one can be chosen', () => {
|
||||
cy.init('/faculties', 'admin')
|
||||
cy.get('[data-cy=select-faculty]').contains('td', 'H10')
|
||||
cy.contains('td', 'H99').should('not.exist')
|
||||
cy.contains('td', 'H90').click()
|
||||
cy.contains('.header', 'Eläinlääketieteellinen tiedekunta')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Faculty basic information: admin user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties', 'admin')
|
||||
cy.contains('td', 'H90').click()
|
||||
})
|
||||
it('Credits produced by faculty are shown', () => {
|
||||
cy.get('[data-cy="Section-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
})
|
||||
|
||||
it('Update statistics tab is shown', () => {
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Update statistics')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Faculty basic information: basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.contains('td', 'H80').click()
|
||||
})
|
||||
it('Basic information tab show all graphs and tables', () => {
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-ThesisWritersOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-StudentsOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-GraduatedOfTheFaculty"]').should('be.visible')
|
||||
})
|
||||
|
||||
it('Correct tabs are shown', () => {
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Basic information')
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Graduation times')
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Programmes and student populations')
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('not.contain', 'Update statistics')
|
||||
})
|
||||
|
||||
it('Toggle years works', () => {
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('contain', '2022')
|
||||
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('contain', '2022')
|
||||
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('contain', '2022')
|
||||
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('contain', '2022')
|
||||
cy.get('[data-cy="YearToggle"]').click()
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('contain', '2022 - 2023')
|
||||
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('contain', '2022 - 2023')
|
||||
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('contain', '2022 - 2023')
|
||||
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('contain', '2022 - 2023')
|
||||
})
|
||||
|
||||
it('Toggle programmes works', () => {
|
||||
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('not.exist')
|
||||
cy.get('[data-cy="ProgrammeToggle"]').click()
|
||||
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('be.visible')
|
||||
})
|
||||
|
||||
it('Students of the faculty infobox works', () => {
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('not.exist')
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-open-info"]').click()
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('be.visible')
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-close-info"]').click()
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Study programme information: users with access', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.contains('td', 'H80').click()
|
||||
})
|
||||
|
||||
it('Study programme credit information is not visible in the beginning', () => {
|
||||
cy.get('table[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('Study programme credit information can be toggled', () => {
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
|
||||
// I tried to fix this without cy.wait() method without effort. Sorting the programmes
|
||||
// or something else causes rerendering of the view and yield to failing tests.
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-3"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Button-Hide-CreditsProducedByTheFaculty-0"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('Graph stays open when sorted', () => {
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
cy.get('[data-cy="Menu-CreditsProducedByTheFaculty-Total"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Average graduation times', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.contains('td', 'H60').click()
|
||||
cy.contains('Graduation times').click()
|
||||
})
|
||||
|
||||
it('User can view graduation graphs', () => {
|
||||
cy.get('[data-cy="Section-AverageGraduationTimes"]').should('be.visible')
|
||||
cy.get('[data-cy="Section-bachelor"]').should('be.visible')
|
||||
cy.get('[data-cy="Section-master"]').should('be.visible')
|
||||
cy.get('[data-cy="Section-master"]').within(() => {
|
||||
cy.contains('Graduation year').should('be.visible')
|
||||
cy.contains('.message', "Click a bar to view that year's programme level breakdown").should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
it('Graphs display data', () => {
|
||||
cy.get('[data-cy="Section-bachelor"]').within(() => {
|
||||
cy.get('div[class="faculty-graph"]')
|
||||
cy.contains('1 graduated').should('have.length', 1)
|
||||
cy.contains('1 graduated').trigger('mouseover')
|
||||
cy.contains('1 students graduated in year 2019')
|
||||
cy.contains('median study time: 40 months')
|
||||
cy.contains('0 graduated on time')
|
||||
cy.contains('1 graduated max year overtime')
|
||||
|
||||
cy.contains('1 graduated').click()
|
||||
cy.contains('Year 2019 by graduation year')
|
||||
cy.get('div[class="programmes-graph"]').should('be.visible')
|
||||
cy.get('div[class="programmes-graph"]').within(() => {
|
||||
cy.contains('EDUK')
|
||||
cy.contains('1 graduated').trigger('mouseover')
|
||||
cy.contains('Kasvatustieteiden kandiohjelma')
|
||||
cy.contains('KH60_001')
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('[data-cy="Section-master"]').within(() => {
|
||||
cy.get('div[class="faculty-graph"]')
|
||||
cy.contains('1 graduated').should('have.length', 1)
|
||||
cy.contains('1 graduated').trigger('mouseover')
|
||||
cy.contains('1 students graduated in year 2020')
|
||||
cy.contains('median study time: 25 months')
|
||||
cy.contains('0 graduated over year late')
|
||||
cy.contains('1 graduated max year overtime')
|
||||
|
||||
cy.contains('1 graduated').click()
|
||||
cy.contains('Year 2020 by graduation year')
|
||||
cy.get('div[class="programmes-graph"]').should('be.visible')
|
||||
cy.get('div[class="programmes-graph"]').within(() => {
|
||||
cy.contains('EDUM')
|
||||
cy.contains('1 graduated').trigger('mouseover')
|
||||
cy.contains('Kasvatustieteiden maisteriohjelma')
|
||||
cy.contains('MH60_001')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Graduation times grouping and time types can be toggled', () => {
|
||||
cy.get('[data-cy="GroupByToggle"]').click()
|
||||
cy.get('[data-cy="Section-bachelor"]').should('not.exist')
|
||||
cy.get('[data-cy="Section-master"]').should('be.visible')
|
||||
|
||||
cy.get('[data-cy="Section-master"]').within(() => {
|
||||
cy.get('div[class="faculty-graph"]')
|
||||
cy.contains('1 graduated (100 % of class)').should('have.length', 1)
|
||||
cy.contains('1 graduated').trigger('mouseover')
|
||||
cy.contains('From class of 2018, 1/undefined students have graduated')
|
||||
})
|
||||
|
||||
cy.get('[data-cy="GraduationTimeToggle"]').click()
|
||||
cy.get('[data-cy="Section-master"]').within(() => {
|
||||
cy.contains('1 graduated (100 % of class)').click()
|
||||
cy.get('div[class="programmes-graph"]').should('be.visible')
|
||||
cy.get('div[class="programmes-graph"]').within(() => {
|
||||
cy.contains('Year 2018 by start year')
|
||||
cy.contains('1 graduated').trigger('mouseover')
|
||||
cy.contains('From class of 2018, 1/1 students have graduated')
|
||||
cy.contains('mean study time: 25 months')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
// const baseUrl = Cypress.config().baseUrl
|
||||
|
||||
const MOCKED_DATE = new Date(2022, 6, 12).getTime()
|
||||
|
||||
const checkFilteringResult = studentCount => {
|
||||
cy.contains(`Students (${studentCount})`)
|
||||
}
|
||||
@@ -17,23 +19,59 @@ const clearSingleDropdownSelection = dataCyAttribute => {
|
||||
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 (filterName, testStepFunctionToRun) => {
|
||||
cy.get(`[data-cy="${filterName}-header"]`).click()
|
||||
checkFilteringResult(amountWithoutFiltering)
|
||||
testStepFunctionToRun()
|
||||
checkFilteringResult(amountWithoutFiltering)
|
||||
cy.get(`[data-cy="${filterName}-header"]`).click({ force: true })
|
||||
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(() => {
|
||||
@@ -42,28 +80,17 @@ 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.')
|
||||
const card = cy.cs('TransferredToProgramme-filter-card')
|
||||
card.cs('TransferredToProgramme-header').click()
|
||||
card.get('[data-cy="option-havenot"] input').should('be.checked')
|
||||
card.cs('TransferredToProgramme-header').click()
|
||||
})
|
||||
|
||||
it('Programme defaults to "Active Study Right" mode', () => {
|
||||
runTestStepWithPreAndPostParts('Programme', () => {
|
||||
cy.cs('Programme-filter-mode-selector').children().eq(0).should('contain', 'Active Study Right')
|
||||
runTestStepWithPreAndPostParts('TransferredToProgramme', () => {
|
||||
const card = cy.cs('TransferredToProgramme-filter-card')
|
||||
card.get('[data-cy="option-havenot"] input').should('be.checked')
|
||||
})
|
||||
})
|
||||
|
||||
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('Study track filter works', () => {
|
||||
// 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)
|
||||
@@ -74,25 +101,25 @@ describe('Population Statistics', () => {
|
||||
|
||||
it('Graduation filter works', () => {
|
||||
runTestStepWithPreAndPostParts('GraduatedFromProgramme', () => {
|
||||
const card = cy.cs('GraduatedFromProgramme-filter-card')
|
||||
const getCard = () => cy.cs('GraduatedFromProgramme-filter-card')
|
||||
|
||||
const graduated = 11
|
||||
card.cs('option-graduated-true').click()
|
||||
getCard().cs('option-graduated-true').click()
|
||||
checkFilteringResult(graduated)
|
||||
card.cs('option-graduated-false').click()
|
||||
getCard().cs('option-graduated-false').click()
|
||||
checkFilteringResult(defaultAmountOfStudents - graduated)
|
||||
card.get('[data-cy="GraduatedFromProgramme-filter-card"] [data-cy="option-all"]').click()
|
||||
getCard().cs('option-all').click()
|
||||
})
|
||||
})
|
||||
|
||||
it('Transfer filter works', () => {
|
||||
runTestStepWithPreAndPostParts('TransferredToProgramme', () => {
|
||||
const transferred = 1
|
||||
cy.cs('option-have').click()
|
||||
cy.cs('TransferredToProgramme-filter-card').cs('option-have').click()
|
||||
checkFilteringResult(transferred)
|
||||
cy.cs('option-all').click()
|
||||
cy.cs('TransferredToProgramme-filter-card').cs('option-all').click()
|
||||
checkFilteringResult(defaultAmountOfStudents + transferred)
|
||||
cy.cs('option-havenot').click()
|
||||
cy.cs('TransferredToProgramme-filter-card').cs('option-havenot').click()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -100,37 +127,30 @@ describe('Population Statistics', () => {
|
||||
runTestStepWithPreAndPostParts('EnrollmentStatus', () => {
|
||||
cy.cs('enrollmentStatusFilter-status').selectFromDropdown(0)
|
||||
cy.cs('enrollmentStatusFilter-semesters').selectFromDropdown(0)
|
||||
checkFilteringResult(137)
|
||||
checkFilteringResult(148)
|
||||
clearSemanticUIMultipleDropDownSelection('enrollmentStatusFilter-semesters')
|
||||
})
|
||||
})
|
||||
|
||||
it('Credit filter works', () => {
|
||||
runTestStepWithPreAndPostParts('CreditsEarned', () => {
|
||||
cy.cs('credit-filter-min').type('50')
|
||||
cy.cs('credit-filter-max').type('150')
|
||||
checkFilteringResult(116)
|
||||
cy.cs('credit-filter-min').clear()
|
||||
cy.cs('credit-filter-max').clear()
|
||||
testRangeFilter('CreditsEarned-filter-card', 50, 150, 118)
|
||||
})
|
||||
})
|
||||
|
||||
it('Age filter works', () => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
cy.cs('ageFilter-min').type('20')
|
||||
cy.cs('ageFilter-max').type('40')
|
||||
checkFilteringResult(41)
|
||||
cy.cs('ageFilter-min').find('input').clear()
|
||||
cy.cs('ageFilter-max').find('input').clear()
|
||||
testRangeFilter('Age-filter-card', 20, 40, 47)
|
||||
})
|
||||
})
|
||||
|
||||
it('Gender filter works', () => {
|
||||
runTestStepWithPreAndPostParts('Gender', () => {
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(0)
|
||||
checkFilteringResult(36)
|
||||
checkFilteringResult(42)
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(1)
|
||||
checkFilteringResult(112)
|
||||
checkFilteringResult(117)
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(2)
|
||||
checkFilteringResult(0)
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(3)
|
||||
@@ -144,8 +164,8 @@ describe('Population Statistics', () => {
|
||||
cy.cs('startYearAtUni-dropdown').selectFromDropdown(0)
|
||||
checkFilteringResult(1)
|
||||
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
|
||||
cy.cs('startYearAtUni-dropdown').selectFromDropdown(14)
|
||||
checkFilteringResult(121)
|
||||
cy.cs('startYearAtUni-dropdown').selectFromDropdown(7)
|
||||
checkFilteringResult(152)
|
||||
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
|
||||
})
|
||||
})
|
||||
@@ -181,16 +201,13 @@ describe('Population Statistics', () => {
|
||||
})
|
||||
|
||||
it('Filter combinations work', () => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
runTestStepWithPreAndPostParts('GraduatedFromProgramme', () => {
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
const card = cy.cs('GraduatedFromProgramme-filter-card')
|
||||
card.cs('option-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()
|
||||
card.cs('option-all').click()
|
||||
const getCard = () => cy.cs('GraduatedFromProgramme-filter-card')
|
||||
getCard().cs('option-graduated-true').click()
|
||||
testRangeFilter('Age-filter-card', 20, 30, 1)
|
||||
getCard().cs('option-all').click()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -198,7 +215,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)
|
||||
|
||||
@@ -232,7 +249,10 @@ describe('Course Statistics', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('Study track filter works', () => {
|
||||
// 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)
|
||||
@@ -242,12 +262,9 @@ describe('Course Statistics', () => {
|
||||
})
|
||||
|
||||
it('Age filter works', () => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
cy.cs('ageFilter-min').type('20')
|
||||
cy.cs('ageFilter-max').type('40')
|
||||
checkFilteringResult(32)
|
||||
cy.cs('ageFilter-min').find('input').clear()
|
||||
cy.cs('ageFilter-max').find('input').clear()
|
||||
testRangeFilter('Age-filter-card', 20, 40, 33)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -270,21 +287,18 @@ describe('Course Statistics', () => {
|
||||
cy.cs('startYearAtUni-dropdown').selectFromDropdown([0])
|
||||
checkFilteringResult(1)
|
||||
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
|
||||
cy.cs('startYearAtUni-dropdown').selectFromDropdown(12)
|
||||
checkFilteringResult(29)
|
||||
cy.cs('startYearAtUni-dropdown').selectFromDropdown(3)
|
||||
checkFilteringResult(84)
|
||||
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
|
||||
})
|
||||
})
|
||||
|
||||
it('Filter combinations work', () => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -307,12 +321,9 @@ describe('Custom Population Statistics', () => {
|
||||
})
|
||||
|
||||
it('Age filter works', () => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
+73
-42
@@ -1,17 +1,18 @@
|
||||
/// <reference types="Cypress" />
|
||||
// Now "Class statistics" in UI
|
||||
|
||||
const moment = require('moment')
|
||||
const _ = require('lodash')
|
||||
|
||||
const setPopStatsUntil = (until, includeSettings = []) => {
|
||||
cy.contains('Advanced settings').siblings().get('[data-cy=advanced-toggle]').click()
|
||||
includeSettings.forEach(setting => {
|
||||
cy.contains('Advanced settings').parent().siblings().contains(setting).click()
|
||||
})
|
||||
cy.get('.adv-stats-until > .form-control').click().clear().type(until)
|
||||
cy.contains('Fetch population with new settings').click()
|
||||
cy.contains('Advanced settings')
|
||||
}
|
||||
// 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 =
|
||||
@@ -24,8 +25,8 @@ describe('Population Statistics tests', () => {
|
||||
})
|
||||
|
||||
it('Population statistics search form is usable', () => {
|
||||
cy.contains('See population').should('be.disabled')
|
||||
cy.contains('Search for population')
|
||||
cy.contains('See class').should('be.disabled')
|
||||
cy.contains('Search for class')
|
||||
cy.contains('Class of')
|
||||
.parent()
|
||||
.within(() => {
|
||||
@@ -47,7 +48,7 @@ describe('Population Statistics tests', () => {
|
||||
.children()
|
||||
.contains('Tietojenkäsittelytieteen maisteriohjelma')
|
||||
.click()
|
||||
cy.contains('See population').should('be.enabled')
|
||||
cy.contains('See class').should('be.enabled')
|
||||
})
|
||||
|
||||
it('Searching for population really shows population', () => {
|
||||
@@ -57,40 +58,42 @@ describe('Population Statistics tests', () => {
|
||||
.children()
|
||||
.contains('Tietojenkäsittelytieteen kandiohjelma')
|
||||
.click()
|
||||
cy.contains('See population').click()
|
||||
cy.contains('Population statistics')
|
||||
cy.contains('See class').click()
|
||||
cy.contains('class size')
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
|
||||
})
|
||||
|
||||
it('Population statistics is usable on general level', () => {
|
||||
cy.visit(pathToCSBach2017)
|
||||
cy.cs('filtered-students')
|
||||
setPopStatsUntil('toukokuu 2020')
|
||||
|
||||
cy.get('.card').within(() => {
|
||||
// This advanced settings does not work correctly
|
||||
// see https://github.com/UniversityOfHelsinkiCS/oodikone/issues/3552
|
||||
// setPopStatsUntil('toukokuu 2020')
|
||||
cy.get('.header').within(() => {
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
|
||||
cy.contains('Sample size: 149 students')
|
||||
cy.contains('class size 170 students')
|
||||
})
|
||||
cy.get('.card').within(() => {
|
||||
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.cs('filtered-students')
|
||||
cy.contains('Courses of population').click()
|
||||
cy.contains('Courses of class').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()
|
||||
@@ -105,9 +108,10 @@ describe('Population Statistics tests', () => {
|
||||
cy.contains('Statistics until')
|
||||
// only spring
|
||||
cy.cs('toggle-fall').click()
|
||||
cy.contains('Fetch population').click()
|
||||
cy.contains('Fetch class').click()
|
||||
|
||||
// moar waiting hack
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
|
||||
cy.contains('No statistics found for the given query.')
|
||||
@@ -117,15 +121,14 @@ describe('Population Statistics tests', () => {
|
||||
cy.cs('toggle-fall').click()
|
||||
cy.cs('toggle-spring').click()
|
||||
|
||||
cy.contains('Fetch population').click()
|
||||
cy.contains('Fetch class').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('Fetch class').click()
|
||||
|
||||
cy.contains('Credit accumulation (for 170 students)')
|
||||
})
|
||||
@@ -133,14 +136,14 @@ describe('Population Statistics tests', () => {
|
||||
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())
|
||||
|
||||
cy.contains('Credits Gained').click()
|
||||
cy.get("[data-cy='credits-gained-main-table']").should('contain', 'All students of the class')
|
||||
const today = moment().endOf('month')
|
||||
const months = Math.round(moment.duration(moment(today).diff(moment('2017-08-01'))).asMonths())
|
||||
const limits = [1, ..._.range(1, 4).map(p => Math.ceil(months * ((p * 15) / 12))), null]
|
||||
const ranges = _.range(1, limits.length).map(i => _.slice(limits, i - 1, i + 1))
|
||||
|
||||
cy.get('.credits-gained-table').should('contain', '(n=149)')
|
||||
cy.get('.credits-gained-table').should('contain', '(n=170)')
|
||||
|
||||
for (const [start, end] of ranges) {
|
||||
let value = 'credits'
|
||||
@@ -159,7 +162,7 @@ describe('Population Statistics tests', () => {
|
||||
cy.get("[data-cy='credits-gained-table-body'] td:nth-child(3)").then($els => {
|
||||
const sum = [...$els].map($el => parseInt($el.innerText)).reduce((a, b) => a + b, 0)
|
||||
|
||||
expect(sum).to.equal(149)
|
||||
expect(sum).to.equal(170)
|
||||
})
|
||||
|
||||
cy.get("[data-cy='credits-gained-table-Ei valintatapaa']").should('not.exist')
|
||||
@@ -170,6 +173,7 @@ describe('Population Statistics tests', () => {
|
||||
cy.cs('filtered-students')
|
||||
cy.contains('Credit statistics')
|
||||
cy.get("[data-cy='credit-statistics']").click()
|
||||
cy.contains('Credits Gained').click()
|
||||
cy.get('.credits-gained-divider').click()
|
||||
cy.get("[data-cy='credits-gained-table-Avoin väylä']").should('exist')
|
||||
cy.get("[data-cy='credits-gained-table-Yhteispisteet']").should('exist')
|
||||
@@ -182,10 +186,10 @@ describe('Population Statistics tests', () => {
|
||||
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.74')
|
||||
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')
|
||||
})
|
||||
})
|
||||
@@ -198,7 +202,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()
|
||||
@@ -215,4 +219,31 @@ describe('Population Statistics tests', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when using IAM user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init(pathToCSBach2017, 'onlyiamrights')
|
||||
})
|
||||
|
||||
it('Population statistics is visible', () => {
|
||||
cy.reload()
|
||||
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
|
||||
cy.contains('class size 170 students')
|
||||
|
||||
cy.get('.card').within(() => {
|
||||
cy.contains('Excludes exchange students')
|
||||
cy.contains('Excludes students with non-degree study right')
|
||||
cy.contains('Excludes students who have transferred out of this programme')
|
||||
})
|
||||
|
||||
cy.contains('Credit accumulation (for 170 students)')
|
||||
cy.contains('Credit statistics')
|
||||
})
|
||||
|
||||
it('Students tab is not available', () => {
|
||||
cy.contains('Students (170)').should('not.exist')
|
||||
// TODO: somehow check that obfuscation works
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -11,6 +11,7 @@ describe('Student Statistics tests', () => {
|
||||
firstnames: 'Matti Jaakko',
|
||||
lastname: 'Nieminen',
|
||||
studentnumber: '010654019',
|
||||
sis_person_id: 'hy-hlo-84830887',
|
||||
}
|
||||
|
||||
it('Student statistics search form is usable', () => {
|
||||
@@ -37,9 +38,9 @@ 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('Tietojenkäsittelytieteen kandiohjelma (01.08.2017 - 01.08.2025')
|
||||
cy.contains('td a', student.studentnumber).click()
|
||||
cy.contains('Started: 01.08.2017')
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma (01.08.2017 - 31.07.2025')
|
||||
cy.contains(student.lastname).should('not.exist')
|
||||
cy.contains(student.firstnames).should('not.exist')
|
||||
|
||||
@@ -54,7 +55,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 +63,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(() => {
|
||||
@@ -72,6 +73,14 @@ describe('Student Statistics tests', () => {
|
||||
cy.contains('Introduction to Machine Learning')
|
||||
})
|
||||
|
||||
it('Has correct Sisu link', () => {
|
||||
cy.get('.prompt').type(student.studentnumber)
|
||||
cy.contains('td a', student.studentnumber).click()
|
||||
cy.get('[data-cy=sisulink] > a')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', 'https://sis-helsinki.funidata.fi/tutor/role/staff/student/hy-hlo-84830887/basic/basic-info')
|
||||
})
|
||||
|
||||
it('Searching with bad inputs doesnt yield results', () => {
|
||||
cy.get('.prompt').type('SWAG LITTINEN')
|
||||
cy.contains('Student number').should('not.exist')
|
||||
@@ -82,10 +91,10 @@ describe('Student Statistics tests', () => {
|
||||
|
||||
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')
|
||||
cy.contains('class size')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
const deleteTag = name => {
|
||||
cy.contains('td', name).siblings().contains('Delete').click()
|
||||
cy.contains('Are you sure you want to delete tag')
|
||||
cy.contains('Confirm').click()
|
||||
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', () => {
|
||||
/* Basic information overview -tests*/
|
||||
describe('Basic information -view works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click({ force: true })
|
||||
// cy.viewport(1536, 960)
|
||||
})
|
||||
|
||||
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
|
||||
it('Basic information -tab loads', () => {
|
||||
cy.get('[data-cy=Section-StudentsOfTheStudyprogramme]')
|
||||
cy.get('[data-cy=Section-CreditsProducedByTheStudyprogramme]')
|
||||
cy.get('[data-cy=Section-GraduatedAndThesisWritersOfTheProgramme]')
|
||||
cy.get('[data-cy=Section-ProgrammesBeforeOrAfter]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimes]')
|
||||
})
|
||||
|
||||
it('Basic information contains correct students', () => {
|
||||
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, 35, 3, 2],
|
||||
[2019, 87, 17, 0, 1],
|
||||
[2018, 161, 1, 0, 0],
|
||||
[2017, 171, 0, 0, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudentsOfTheStudyprogramme')
|
||||
})
|
||||
|
||||
it('Basic information contains correct credits', () => {
|
||||
const years = getEmptyYears()
|
||||
const tableContents = [
|
||||
// [Year, Total, Major credits, Non-major credits, Non-degree credits, Transferred credits]
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0]),
|
||||
[2021, 0, 0, 0, 0, 0],
|
||||
[2020, 5860, 5591, 102, 0, 167],
|
||||
[2019, 8696, 7737, 577, 33, 349],
|
||||
[2018, 7818, 6584, 852, 110, 272],
|
||||
[2017, 3085, 2492, 153, 52, 388],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
})
|
||||
|
||||
it('Basic information contains correct thesis writers and graduates', () => {
|
||||
const years = getEmptyYears()
|
||||
const tableContents = [
|
||||
// [Year, Graduated, Wrote thesis]
|
||||
...years.map(year => [year, 0, 0]),
|
||||
[2021, 0, 0],
|
||||
[2020, 35, 35],
|
||||
[2019, 17, 25],
|
||||
[2018, 1, 5],
|
||||
[2017, 0, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'GraduatedAndThesisWritersOfTheProgramme')
|
||||
})
|
||||
|
||||
it.skip('Special studyrights can be excluded and basic data changes accordingly', () => {
|
||||
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', () => {
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.get('[data-cy=YearToggle]').click({ force: true })
|
||||
const isAcademicYear = true
|
||||
const years = getEmptyYears(isAcademicYear)
|
||||
const studentTableContents = [
|
||||
// [Year, Started, Graduated, Transferred away, Transferred to]
|
||||
...years.map(year => [year, 0, 0, 0, 0]),
|
||||
['2021 - 2022', 0, 0, 0, 0],
|
||||
['2020 - 2021', 12, 6, 3, 2],
|
||||
['2019 - 2020', 86, 41, 1, 0],
|
||||
['2018 - 2019', 162, 5, 0, 1],
|
||||
['2017 - 2018', 171, 1, 0, 0],
|
||||
]
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
|
||||
|
||||
const creditTableContents = [
|
||||
// [Year, Total, Major students credits, Non-major students credits, Non-degree credits, Transferred credits]
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0]),
|
||||
['2021 - 2022', 0, 0, 0, 0, 0],
|
||||
['2020 - 2021', 566, 486, 20, 0, 60],
|
||||
['2019 - 2020', 8595, 8271, 106, 0, 218],
|
||||
['2018 - 2019', 9992, 8855, 781, 53, 303],
|
||||
['2017 - 2018', 6055, 4792, 777, 142, 344],
|
||||
]
|
||||
|
||||
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
//cy.wait(10000)
|
||||
cy.get('[data-cy=YearToggle]').click()
|
||||
})
|
||||
|
||||
it('Basic information graphs render', () => {
|
||||
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', 852)
|
||||
.should('contain', 577)
|
||||
|
||||
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', 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)
|
||||
})
|
||||
})
|
||||
|
||||
/* Studytrack overview -tests*/
|
||||
describe('Studytrack overview works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
})
|
||||
|
||||
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
|
||||
it('Studytracks and student populations -tab loads', () => {
|
||||
cy.get('[data-cy=Section-StudytrackOverview]')
|
||||
cy.get('[data-cy=Section-StudytrackProgress]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimes]')
|
||||
})
|
||||
|
||||
it('Students of the studyprogramme are shown correctly', () => {
|
||||
const tableContents = [
|
||||
// [Year, All, Started studying, Currently enrolled, Absent, Cancelled, Graduated, Men, Women, Finnish]
|
||||
['2020 - 2021', 12, 10, 0, 0, 0, 0, 10, 2, 12],
|
||||
['2019 - 2020', 86, 86, 0, 0, 1, 1, 65, 21, 86],
|
||||
['2018 - 2019', 162, 161, 0, 0, 0, 11, 118, 44, 161],
|
||||
['2017 - 2018', 171, 171, 0, 0, 4, 42, 135, 36, 166],
|
||||
['Total', 431, 431, 0, 0, 5, 54, 328, 103, 425],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudytrackOverview')
|
||||
})
|
||||
|
||||
it('Student progress data is shown correctly', () => {
|
||||
const tableContents = [
|
||||
['2020 - 2021', 12, 12, 0, 0, 0, 0, 0, 0, 0],
|
||||
['2019 - 2020', 86, 11, 15, 35, 20, 5, 0, 0, 0],
|
||||
['2018 - 2019', 162, 3, 9, 28, 50, 33, 25, 11, 3],
|
||||
['2017 - 2018', 171, 7, 13, 22, 32, 18, 23, 32, 24],
|
||||
['Total', 431, 16, 35, 65, 103, 75, 56, 50, 31],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudytrackProgress')
|
||||
})
|
||||
|
||||
it('Studytrack overview graphs render', () => {
|
||||
cy.get('[data-cy=Graph-StudytrackProgress]')
|
||||
.should('contain', 'Less than 15 credits')
|
||||
.should('contain', '30-59 credits')
|
||||
.should('contain', '180 or more credits')
|
||||
.should('contain', '4.1%') // The percentage for less than 15 credits in 2017-2018, to check that the graph renders
|
||||
.should('contain', '5.6%') // The percentage for 15-29 credits in 2018-2019
|
||||
.should('contain', '12.8%') // The percentage for less than 15 credits in 2019-2020
|
||||
|
||||
cy.get('[data-cy=Graph-2018-AverageGraduationTimes]')
|
||||
.should('contain', '2018 - 2019')
|
||||
.should('contain', 'n = 11 / 162')
|
||||
.should('contain', '22 kk')
|
||||
})
|
||||
})
|
||||
|
||||
/* Programme courses -tests*/
|
||||
describe('Programme courses works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Programme courses').click()
|
||||
})
|
||||
|
||||
it('content loads', () => {
|
||||
cy.get('[data-cy=CoursesYearFilter]')
|
||||
cy.get('[data-cy=CourseTabs]')
|
||||
})
|
||||
|
||||
it('time range selection works', () => {
|
||||
cy.get('[data-cy=fromYear]').click().contains('2018').click()
|
||||
|
||||
cy.get('[data-cy=toYear').click().contains('2019').click()
|
||||
|
||||
cy.get('[data-cy=CoursesSortableTable]').within(() => {
|
||||
cy.get('tr').eq(1).contains('684')
|
||||
})
|
||||
})
|
||||
|
||||
it('calendar year -> academic year toggle works', () => {
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
cy.get('[data-cy=fromYear]').click().contains('2018').click()
|
||||
|
||||
cy.get('[data-cy=toYear').click().contains('2019').click()
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(20000)
|
||||
cy.get('[data-cy=CoursesSortableTable]').within(() => {
|
||||
cy.get('tr').eq(1).contains('684')
|
||||
})
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(20000)
|
||||
cy.get('[data-cy=calendarAcademicYearToggle]').first().click()
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(20000)
|
||||
cy.get('[data-cy=CoursesSortableTable]').within(() => {
|
||||
cy.get('tr').eq(1).contains('772')
|
||||
})
|
||||
})
|
||||
|
||||
it('contains correct courses in alphabetical order', () => {
|
||||
cy.get('[data-cy=CoursesSortableTable]').within(() => {
|
||||
cy.get('tr').eq(1).contains('Aineopintojen harjoitustyö: Tietokantasovellus')
|
||||
cy.get('tr').eq(-1).contains('Äidinkielinen viestintä')
|
||||
})
|
||||
})
|
||||
|
||||
it('different sorting options work', () => {
|
||||
cy.get('[data-cy=CoursesSortableTable]').within(() => {
|
||||
// Test sorting by different columns
|
||||
cy.get('th').eq(0).click()
|
||||
cy.get('tr').eq(2).contains('Tietojenkäsittelytieteen kisälliopetus: Java-ohjelmointi')
|
||||
cy.get('th').eq(1).click()
|
||||
cy.get('tr').eq(1).contains('Äidinkielinen viestintä')
|
||||
cy.get('th').eq(2).click()
|
||||
cy.get('tr').eq(1).contains('Tietorakenteet ja algoritmit')
|
||||
})
|
||||
})
|
||||
|
||||
it('show credits -> show students toggle works', () => {
|
||||
cy.get('[data-cy=CoursesSortableTable]').within(() => {
|
||||
cy.get('tr').eq(1).contains('1068')
|
||||
})
|
||||
|
||||
cy.get('[data-cy=creditsStudentsToggle]').first().click()
|
||||
|
||||
cy.get('[data-cy=CoursesSortableTable]').within(() => {
|
||||
cy.get('tr').eq(1).contains('267')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/* Tag-tests*/
|
||||
describe('Basic information -view works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Tags').click()
|
||||
})
|
||||
|
||||
it('can create and delete tags for population', () => {
|
||||
const name = `tag-${new Date().getTime()}`
|
||||
cy.get('.tagNameSelectInput > .ui > input').type(name)
|
||||
cy.get('.yearSelectInput').type('2018')
|
||||
cy.contains('Create new tag').click()
|
||||
cy.contains(name)
|
||||
cy.contains('2018')
|
||||
deleteTag(name)
|
||||
})
|
||||
|
||||
it('can create personal tags', () => {
|
||||
const name = `tag-${new Date().getTime()}`
|
||||
cy.get('.tagNameSelectInput > .ui > input').type(name)
|
||||
cy.get('.yearSelectInput').type('2018')
|
||||
|
||||
cy.get('.ui > label').click()
|
||||
cy.contains('Create new tag').click()
|
||||
cy.contains(name)
|
||||
deleteTag(name)
|
||||
})
|
||||
|
||||
it('can add tags to students', () => {
|
||||
const name = `tag-${new Date().getTime()}`
|
||||
|
||||
const student = '010113437'
|
||||
cy.get('.tagNameSelectInput > .ui > input').type(name)
|
||||
cy.get('.yearSelectInput').type('2018')
|
||||
cy.contains('Create new tag').click()
|
||||
cy.contains(name)
|
||||
|
||||
cy.contains('Add tags to students').click()
|
||||
cy.get('.form > .field > .dropdown').click().get('.ui > input.search').type(name).click()
|
||||
|
||||
cy.get('.form > .field > .dropdown > .visible').contains(name).click()
|
||||
|
||||
cy.get('textarea').type('010113437')
|
||||
cy.get('.positive').click()
|
||||
|
||||
cy.contains('Student statistics').click()
|
||||
cy.get('.prompt').type(student)
|
||||
cy.contains('a', student).click()
|
||||
cy.contains(name)
|
||||
|
||||
cy.go('back')
|
||||
cy.go('back')
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
deleteTag(name)
|
||||
|
||||
cy.contains('Student statistics').click()
|
||||
cy.get('.prompt').type(student)
|
||||
cy.contains('a', student).click()
|
||||
cy.contains(name).should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('IAM user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme', 'onlyiamrights')
|
||||
})
|
||||
|
||||
it('can access programme and correct tabs are visible', () => {
|
||||
cy.reload()
|
||||
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
|
||||
cy.contains('Basic information')
|
||||
cy.contains('Studytracks and student populations')
|
||||
|
||||
cy.contains('Update statistics').should('not.exist')
|
||||
cy.contains('Degree Courses').should('not.exist')
|
||||
})
|
||||
|
||||
it('can access basic information', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.contains('Basic information').click()
|
||||
|
||||
cy.get('[data-cy=Section-StudentsOfTheStudyprogramme]')
|
||||
cy.get('[data-cy=Section-CreditsProducedByTheStudyprogramme]')
|
||||
cy.get('[data-cy=Section-GraduatedAndThesisWritersOfTheProgramme]')
|
||||
cy.get('[data-cy=Section-ProgrammesBeforeOrAfter]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimes]')
|
||||
})
|
||||
|
||||
it('can access studytracks', () => {
|
||||
cy.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]')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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 })
|
||||
})
|
||||
})
|
||||
@@ -39,9 +39,7 @@ describe('Users tests', () => {
|
||||
|
||||
it("mocking normal user shows only the mocked user's programmes", () => {
|
||||
cy.contains('mocking').should('not.exist')
|
||||
cy.contains('tr', 'basic').within(() => {
|
||||
cy.contains('.button', 'Edit').click()
|
||||
})
|
||||
cy.cs('user-edit-button-basic').click()
|
||||
cy.get('i.spy').click()
|
||||
cy.contains('mocking as basic')
|
||||
cy.wait(1000)
|
||||
@@ -1,228 +0,0 @@
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
const deleteTag = name => {
|
||||
cy.contains(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')
|
||||
}
|
||||
|
||||
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', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme/KH50_005', 'admin')
|
||||
})
|
||||
|
||||
it('progress should not be recalculating when opened for the first time', () => {
|
||||
cy.contains('Recalculating').should('not.exist')
|
||||
cy.contains('Last updated').should('exist')
|
||||
})
|
||||
|
||||
// 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('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')
|
||||
})
|
||||
})
|
||||
|
||||
it('renders progress and productivity tables with calculated status after recalculating stats again', () => {
|
||||
doRecalculation()
|
||||
checkProgressAndProductivity('admin')
|
||||
|
||||
// Check new calculation statuses are reported
|
||||
const newProgressCalculatedTextElement = cy.cs('throughputUpdateStatus').invoke('text')
|
||||
const newProductivityCalculatedTextElement = cy.cs('productivityUpdateStatus').invoke('text')
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when opening programme page with basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme/KH50_005')
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
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('can move to Population statistics page by clickin', () => {
|
||||
cy.get('i.level.up.alternate.icon').eq(0).click()
|
||||
cy.contains('Students (10)')
|
||||
})
|
||||
|
||||
it('can create and delete tags for population', () => {
|
||||
const name = `tag-${new Date().getTime()}`
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Tags').click()
|
||||
cy.get('.tagNameSelectInput > .ui > input').type(name)
|
||||
cy.get('.yearSelectInput').type('2018')
|
||||
cy.contains('Create new tag').click()
|
||||
cy.contains(name)
|
||||
cy.contains('2018')
|
||||
deleteTag(name)
|
||||
})
|
||||
|
||||
it('can create personal tags', () => {
|
||||
const name = `tag-${new Date().getTime()}`
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Tags').click()
|
||||
cy.get('.tagNameSelectInput > .ui > input').type(name)
|
||||
cy.get('.yearSelectInput').type('2018')
|
||||
|
||||
cy.get('.ui > label').click()
|
||||
cy.contains('Create new tag').click()
|
||||
cy.contains(name)
|
||||
deleteTag(name)
|
||||
})
|
||||
|
||||
it('can add tags to students', () => {
|
||||
const name = `tag-${new Date().getTime()}`
|
||||
|
||||
const student = '010113437'
|
||||
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Tags').click()
|
||||
cy.get('.tagNameSelectInput > .ui > input').type(name)
|
||||
cy.get('.yearSelectInput').type('2018')
|
||||
cy.contains('Create new tag').click()
|
||||
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 > .visible').contains(name).click()
|
||||
|
||||
cy.get('textarea').type('010113437')
|
||||
cy.get('.positive').click()
|
||||
|
||||
cy.contains('Student statistics').click()
|
||||
cy.get('.prompt').type(student)
|
||||
cy.contains(student).click()
|
||||
cy.contains(name)
|
||||
|
||||
cy.go('back')
|
||||
cy.go('back')
|
||||
|
||||
deleteTag(name)
|
||||
|
||||
cy.contains('Student statistics').click()
|
||||
cy.get('.prompt').type(student)
|
||||
cy.contains(student).click()
|
||||
cy.contains(name).should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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',
|
||||
@@ -43,7 +43,23 @@ const norightsUserHeaders = {
|
||||
hypersonsisuid: 'hy-hlo-0000000',
|
||||
}
|
||||
|
||||
const userHeaders = [adminUserHeaders, basicUserHeaders, onlycoursestatisticsUserHeaders, norightsUserHeaders]
|
||||
const onlyIamRightsUserHeaders = {
|
||||
uid: 'onlyiamrights',
|
||||
displayname: 'Only IAM rights user',
|
||||
'shib-session-id': 'mock-cypress-session',
|
||||
hygroupcn: 'hy-employees;hy-mltdk-tkt-jory',
|
||||
edupersonaffiliation: 'member',
|
||||
mail: 'grp-toska+mockonlyiamrightsuser@helsinki.fi',
|
||||
hypersonsisuid: 'hy-hlo-1111111',
|
||||
}
|
||||
|
||||
const userHeaders = [
|
||||
adminUserHeaders,
|
||||
basicUserHeaders,
|
||||
onlycoursestatisticsUserHeaders,
|
||||
norightsUserHeaders,
|
||||
onlyIamRightsUserHeaders,
|
||||
]
|
||||
|
||||
/**
|
||||
Set up headers to login, set up correct user (admin / basic / etc.) and open given path.
|
||||
@@ -62,9 +78,13 @@ Cypress.Commands.add('init', (path = '', userId = 'basic') => {
|
||||
* Shorthand for using "Cypress Selectors" (CS), i.e., `data-cy` attributes.
|
||||
*/
|
||||
Cypress.Commands.add('cs', { prevSubject: 'optional' }, (subject, name) => {
|
||||
const p = subject === undefined ? cy : cy.wrap(subject)
|
||||
const selector = `[data-cy="${name}"]`
|
||||
|
||||
return p.get(`[data-cy='${name}']`)
|
||||
if (subject === undefined) {
|
||||
return cy.get(selector)
|
||||
} else {
|
||||
return cy.wrap(subject).find(selector)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -98,5 +118,20 @@ Cypress.Commands.add('selectStudyProgramme', name => {
|
||||
cy.cs('navbar-studyProgramme').click()
|
||||
cy.cs('navbar-class').click()
|
||||
cy.cs('select-study-programme').click().children().contains(name).click()
|
||||
cy.contains('See population').click()
|
||||
cy.contains('See class').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
|
||||
}
|
||||
})
|
||||
+27
-1
@@ -103,9 +103,35 @@ services:
|
||||
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_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:
|
||||
|
||||
+5
-1
@@ -1,3 +1,5 @@
|
||||
version: '2.1'
|
||||
|
||||
x-defaults: &defaults
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -97,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:
|
||||
@@ -122,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:
|
||||
|
||||
Generated
+9265
-2354
File diff suppressed because it is too large
Load Diff
+26
-22
@@ -6,6 +6,7 @@
|
||||
"concurrently": "concurrently",
|
||||
"eslint": "eslint",
|
||||
"stylelint": "stylelint",
|
||||
"actionlint": "node-actionlint",
|
||||
"prettier": "prettier",
|
||||
"cli": "./cli.sh",
|
||||
"oodikone": "./run.sh oodikone anon up --detach",
|
||||
@@ -15,7 +16,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,31 +29,33 @@
|
||||
},
|
||||
"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",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"@babel/core": "^7.19.3",
|
||||
"@babel/eslint-parser": "^7.19.1",
|
||||
"@babel/plugin-syntax-jsx": "^7.16.7",
|
||||
"concurrently": "^7.4.0",
|
||||
"cypress": "^10.9.0",
|
||||
"eslint": "^8.25.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"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",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-prettier": "^8.0.2",
|
||||
"stylelint-config-standard": "^22.0.0",
|
||||
"stylelint-prettier": "^1.2.0"
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.31.9",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"husky": "^8.0.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"node-actionlint": "^1.2.2",
|
||||
"prettier": "^2.5.1",
|
||||
"stylelint": "^14.13.0",
|
||||
"stylelint-config-prettier": "^9.0.3",
|
||||
"stylelint-config-standard": "^26.0.0",
|
||||
"stylelint-prettier": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1"
|
||||
"moment": "^2.29.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -0,0 +1,438 @@
|
||||
const { facultyMap } = require('./data')
|
||||
|
||||
const joryMap = {
|
||||
'hy-ttdk-tuk-jory': '100-K001',
|
||||
'hy-ttdk-tum-jory': '100-M001',
|
||||
'hy-oiktdk-on-jory': '200-K001',
|
||||
'hy-oiktdk-otm-jory': '200-M001',
|
||||
'hy-oiktdk-ibl-jory': '200-M002',
|
||||
'hy-oiktdk-ggl-jory': '200-M003',
|
||||
'hy-ltdk-psyk-jory': ['300-K001', '300-M004'],
|
||||
'hy-ltdk-logo-jory': ['300-K002', '300-M005'],
|
||||
'hy-ltdk-ll-jory': '300-M001',
|
||||
'hy-ltdk-tmed-jory': '300-M002',
|
||||
'hy-ltdk-hll-jory': '300-M003',
|
||||
'hy-humtdk-filk-jory': '400-K001',
|
||||
'hy-humtdk-ttk-jory': '400-K002',
|
||||
'hy-humtdk-kik-jory': '400-K003',
|
||||
'hy-humtdk-kok-jory': '400-K004',
|
||||
'hy-humtdk-kuka-jory': '400-K005',
|
||||
'hy-humtdk-hisk-jory': '400-K006',
|
||||
'hy-humtdk-ttm-jory': '400-M001',
|
||||
'hy-humtdk-kim-jory': '400-M002',
|
||||
'hy-humtdk-eng-jory': '400-M003',
|
||||
'hy-humtdk-rus-jory': '400-M004',
|
||||
'hy-humtdk-lingdig-jory': '400-M005',
|
||||
'hy-humtdk-tra-jory': '400-M006',
|
||||
'hy-humtdk-suku-jory': '400-M007',
|
||||
'hy-humtdk-nor-jory': '400-M008',
|
||||
'hy-humtdk-kir-jory': '400-M009',
|
||||
'hy-humtdk-kuma-jory': '400-M010',
|
||||
'hy-humtdk-ice-jory': '400-M011',
|
||||
'hy-humtdk-alku-jory': '400-M012',
|
||||
'hy-humtdk-spt-jory': '400-M014',
|
||||
'hy-humtdk-hism-jory': '400-M015',
|
||||
'hy-mltdk-mat-jory': '500-K001',
|
||||
'hy-mltdk-fys-jory': '500-K002',
|
||||
'hy-mltdk-kemk-jory': '500-K003',
|
||||
'hy-mltdk-mfkk-jory': '500-K004',
|
||||
'hy-mltdk-tkt-jory': '500-K005',
|
||||
'hy-mltdk-geok-jory': '500-K006',
|
||||
'hy-mltdk-maa-jory': '500-K007',
|
||||
'hy-mltdk-bsc-jory': '500-K008',
|
||||
'hy-mltdk-mast-jory': '500-M001',
|
||||
'hy-mltdk-lsi-jory': '500-M002',
|
||||
'hy-mltdk-tcm-jory': '500-M003',
|
||||
'hy-mltdk-paras-jory': '500-M004',
|
||||
'hy-mltdk-matres-jory': '500-M005',
|
||||
'hy-mltdk-atm-jory': '500-M006',
|
||||
'hy-mltdk-kem-jory': '500-M007',
|
||||
'hy-mltdk-mfkm-jory': '500-M008',
|
||||
'hy-mltdk-csm-jory': '500-M009',
|
||||
'hy-mltdk-data-jory': '500-M010',
|
||||
'hy-mltdk-geom-jory': '500-M011',
|
||||
'hy-mltdk-geog-jory': '500-M012',
|
||||
'hy-mltdk-usp-jory': '500-M013',
|
||||
'hy-ftdk-farm-jory': '550-K001',
|
||||
'hy-ftdk-prov-jory': '550-M001',
|
||||
'hy-ftdk-mpharm-jory': '550-M002',
|
||||
'hy-bytdk-bio-jory': '570-K001',
|
||||
'hy-bytdk-mole-jory': '570-K002',
|
||||
'hy-bytdk-env-jory': '570-K003',
|
||||
'hy-bytdk-eeb-jory': '570-M001',
|
||||
'hy-bytdk-ips-jory': '570-M002',
|
||||
'hy-bytdk-gmb-jory': '570-M003',
|
||||
'hy-bytdk-neuro-jory': '570-M004',
|
||||
'hy-bytdk-ecgs-jory': '570-M005',
|
||||
'hy-ktdk-eduk-jory': '600-K001',
|
||||
'hy-ktdk-edum-jory': '600-M001',
|
||||
'hy-ktdk-ce-jory': '600-M002',
|
||||
'hy-valttdk-pvk-jory': '700-K001',
|
||||
'hy-valttdk-yk-jory': '700-K002',
|
||||
'hy-valttdk-sosk-jory': '700-K003',
|
||||
'hy-valttdk-ecok-jory': '700-K004',
|
||||
'hy-valttdk-film-jory': '700-M001',
|
||||
'hy-valttdk-pvm-jory': '700-M002',
|
||||
'hy-valttdk-gpc-jory': '700-M003',
|
||||
'hy-valttdk-ym-jory': '700-M004',
|
||||
'hy-valttdk-cos-jory': '700-M005',
|
||||
'hy-valttdk-ens-jory': '700-M006',
|
||||
'hy-valttdk-msv-jory': '700-M007',
|
||||
'hy-valttdk-sosm-jory': '700-M008',
|
||||
'hy-valttdk-econ-jory': '700-M009',
|
||||
'hy-valttdk-sote-jory': '700-M011',
|
||||
'hy-sskh-ksv-jory': '740-K001',
|
||||
'hy-mmtdk-maatk-jory': '800-K001',
|
||||
'hy-mmtdk-metsak-jory': '800-K002',
|
||||
'hy-mmtdk-etk-jory': '800-K003',
|
||||
'hy-mmtdk-yet-jory': '800-K004',
|
||||
'hy-mmtdk-agri-jory': '800-M001',
|
||||
'hy-mmtdk-agere-jory': '800-M002',
|
||||
'hy-mmtdk-for-jory': '800-M003',
|
||||
'hy-mmtdk-food-jory': '800-M004',
|
||||
'hy-mmtdk-hnfb-jory': '800-M005',
|
||||
'hy-mmtdk-ekm-jory': '800-M006',
|
||||
'hy-mmtdk-mmb-jory': '800-M007',
|
||||
'hy-eltdk-elk-jory': '900-K001',
|
||||
'hy-eltdk-ell-jory': '900-M001',
|
||||
'hy-dp-dptheol-jory': 'T920101',
|
||||
'hy-dp-dplaw-jory': 'T920102',
|
||||
'hy-dp-philartsoc-jory': 'T920103',
|
||||
'hy-dp-dphistcult-jory': 'T920104',
|
||||
'hy-dp-dplang-jory': 'T920105',
|
||||
'hy-dp-sky-jory': 'T920106',
|
||||
'hy-dp-dpsocs-jory': 'T920107',
|
||||
'hy-dp-pyam-jory': 'T920108',
|
||||
'hy-dp-dpe-jory': 'T920109',
|
||||
'hy-dp-seduce-jory': 'T920110',
|
||||
'hy-dp-clic-jory': 'T920111',
|
||||
'hy-dp-dpbm-jory': 'T921101',
|
||||
'hy-dp-klto-jory': 'T921102',
|
||||
'hy-dp-docpop-jory': 'T921103',
|
||||
'hy-dp-findos-jory': 'T921104',
|
||||
'hy-dp-dpdr-jory': 'T921105',
|
||||
'hy-dp-ils-jory': 'T921106',
|
||||
'hy-dp-bandm-jory': 'T921107',
|
||||
'hy-dp-cvm-jory': 'T921108',
|
||||
'hy-dp-dphub-jory': 'T921109',
|
||||
'hy-dp-luova-jory': 'T922101',
|
||||
'hy-dp-dpps-jory': 'T922102',
|
||||
'hy-dp-denvi-jory': 'T922103',
|
||||
'hy-dp-agforee-jory': 'T922104',
|
||||
'hy-dp-mbdp-jory': 'T922105',
|
||||
'hy-dp-foodhealth-jory': 'T922106',
|
||||
'hy-dp-papu-jory': 'T923101',
|
||||
'hy-dp-geodoc-jory': 'T923102',
|
||||
'hy-dp-atm-dp-jory': 'T923103',
|
||||
'hy-dp-chems-jory': 'T923104',
|
||||
'hy-dp-domast-jory': 'T923105',
|
||||
'hy-dp-matrena-jory': 'T923106',
|
||||
'hy-dp-docs-jory': 'T923107',
|
||||
}
|
||||
|
||||
const kojoMap = {
|
||||
'hy-ttdk-tuk-jory': 'hy-ttdk-kandi-kojot',
|
||||
'hy-ttdk-tum-jory': 'hy-ttdk-maisteri-kojot',
|
||||
'hy-oiktdk-on-jory': 'hy-oiktdk-kandi-kojot',
|
||||
'hy-oiktdk-otm-jory': 'hy-oiktdk-maisteri-kojot',
|
||||
'hy-oiktdk-ibl-jory': 'hy-oiktdk-maisteri-kojot',
|
||||
'hy-oiktdk-ggl-jory': 'hy-oiktdk-maisteri-kojot',
|
||||
'hy-ltdk-psyk-jory': 'hy-ltdk-maisteri-kojot',
|
||||
'hy-ltdk-logo-jory': 'hy-ltdk-maisteri-kojot',
|
||||
'hy-ltdk-ll-jory': 'hy-ltdk-maisteri-kojot',
|
||||
'hy-ltdk-tmed-jory': 'hy-ltdk-maisteri-kojot',
|
||||
'hy-ltdk-hll-jory': 'hy-ltdk-maisteri-kojot',
|
||||
'hy-humtdk-filk-jory': 'hy-humtdk-kandi-kojot',
|
||||
'hy-humtdk-ttk-jory': 'hy-humtdk-kandi-kojot',
|
||||
'hy-humtdk-kik-jory': 'hy-humtdk-kandi-kojot',
|
||||
'hy-humtdk-kok-jory': 'hy-humtdk-kandi-kojot',
|
||||
'hy-humtdk-kuka-jory': 'hy-humtdk-kandi-kojot',
|
||||
'hy-humtdk-hisk-jory': 'hy-humtdk-kandi-kojot',
|
||||
'hy-humtdk-ttm-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-kim-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-eng-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-rus-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-lingdig-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-tra-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-suku-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-nor-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-kir-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-kuma-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-ice-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-alku-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-spt-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-hism-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-mltdk-mat-jory': 'hy-mltdk-kandi-kojot',
|
||||
'hy-mltdk-fys-jory': 'hy-mltdk-kandi-kojot',
|
||||
'hy-mltdk-kemk-jory': 'hy-mltdk-kandi-kojot',
|
||||
'hy-mltdk-mfkk-jory': 'hy-mltdk-kandi-kojot',
|
||||
'hy-mltdk-tkt-jory': 'hy-mltdk-kandi-kojot',
|
||||
'hy-mltdk-geok-jory': 'hy-mltdk-kandi-kojot',
|
||||
'hy-mltdk-maa-jory': 'hy-mltdk-kandi-kojot',
|
||||
'hy-mltdk-bsc-jory': 'hy-mltdk-kandi-kojot',
|
||||
'hy-mltdk-mast-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-lsi-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-tcm-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-paras-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-matres-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-atm-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-kem-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-mfkm-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-csm-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-data-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-geom-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-geog-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-usp-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-ftdk-farm-jory': 'hy-ftdk-kandi-kojot',
|
||||
'hy-ftdk-prov-jory': 'hy-ftdk-maisteri-kojot',
|
||||
'hy-ftdk-mpharm-jory': 'hy-ftdk-maisteri-kojot',
|
||||
'hy-bytdk-bio-jory': 'hy-bytdk-kandi-kojot',
|
||||
'hy-bytdk-mole-jory': 'hy-bytdk-kandi-kojot',
|
||||
'hy-bytdk-env-jory': 'hy-bytdk-kandi-kojot',
|
||||
'hy-bytdk-eeb-jory': 'hy-bytdk-maisteri-kojot',
|
||||
'hy-bytdk-ips-jory': 'hy-bytdk-maisteri-kojot',
|
||||
'hy-bytdk-gmb-jory': 'hy-bytdk-maisteri-kojot',
|
||||
'hy-bytdk-neuro-jory': 'hy-bytdk-maisteri-kojot',
|
||||
'hy-bytdk-ecgs-jory': 'hy-bytdk-maisteri-kojot',
|
||||
'hy-ktdk-eduk-jory': 'hy-ktdk-kandi-kojot',
|
||||
'hy-ktdk-edum-jory': 'hy-ktdk-maisteri-kojot',
|
||||
'hy-ktdk-ce-jory': 'hy-ktdk-maisteri-kojot',
|
||||
'hy-valttdk-pvk-jory': 'hy-valttdk-kandi-kojot',
|
||||
'hy-valttdk-yk-jory': 'hy-valttdk-kandi-kojot',
|
||||
'hy-valttdk-sosk-jory': 'hy-valttdk-kandi-kojot',
|
||||
'hy-valttdk-ecok-jory': 'hy-valttdk-kandi-kojot',
|
||||
'hy-valttdk-film-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-valttdk-pvm-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-valttdk-gpc-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-valttdk-ym-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-valttdk-cos-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-valttdk-ens-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-valttdk-msv-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-valttdk-sosm-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-valttdk-econ-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-valttdk-sote-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-sskh-ksv-jory': 'hy-sskh-kandi-kojot',
|
||||
'hy-mmtdk-maatk-jory': 'hy-mmtdk-kandi-kojot',
|
||||
'hy-mmtdk-metsak-jory': 'hy-mmtdk-kandi-kojot',
|
||||
'hy-mmtdk-etk-jory': 'hy-mmtdk-kandi-kojot',
|
||||
'hy-mmtdk-yet-jory': 'hy-mmtdk-kandi-kojot',
|
||||
'hy-mmtdk-agri-jory': 'hy-mmtdk-maisteri-kojot',
|
||||
'hy-mmtdk-agere-jory': 'hy-mmtdk-maisteri-kojot',
|
||||
'hy-mmtdk-for-jory': 'hy-mmtdk-maisteri-kojot',
|
||||
'hy-mmtdk-food-jory': 'hy-mmtdk-maisteri-kojot',
|
||||
'hy-mmtdk-hnfb-jory': 'hy-mmtdk-maisteri-kojot',
|
||||
'hy-mmtdk-ekm-jory': 'hy-mmtdk-maisteri-kojot',
|
||||
'hy-mmtdk-mmb-jory': 'hy-mmtdk-maisteri-kojot',
|
||||
'hy-eltdk-elk-jory': 'hy-eltdk-kandi-kojot',
|
||||
'hy-eltdk-ell-jory': 'hy-eltdk-maisteri-kojot',
|
||||
'hy-dp-dptheol-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-dplaw-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-philartsoc-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-dphistcult-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-dplang-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-sky-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-dpsocs-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-pyam-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-dpe-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-seduce-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-clic-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-dpbm-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-klto-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-docpop-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-findos-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-dpdr-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-ils-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-bandm-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-cvm-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-dphub-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-luova-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-dpps-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-denvi-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-agforee-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-mbdp-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-foodhealth-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-papu-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-geodoc-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-atm-dp-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-chems-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-domast-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-matrena-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-docs-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps kosu IAM to the name of TDKs, defined in data.js facultyMap
|
||||
*/
|
||||
const kosuFacultyMap = {
|
||||
'hy-ypa-opa-kosu-kumpula': 'matemaattis-luonnontieteellinen',
|
||||
'hy-ypa-opa-kosu-meilahti': ['lääketieteellinen'],
|
||||
'hy-ypa-opa-kosu-kruununhaka': [
|
||||
'oikeustieteellinen',
|
||||
'valtiotieteellinen',
|
||||
'kasvatustieteellinen',
|
||||
'humanistinen',
|
||||
'teologinen',
|
||||
'svenska',
|
||||
],
|
||||
'hy-ypa-opa-kosu-metsatalo': [
|
||||
'oikeustieteellinen',
|
||||
'valtiotieteellinen',
|
||||
'kasvatustieteellinen',
|
||||
'humanistinen',
|
||||
'teologinen',
|
||||
'svenska',
|
||||
],
|
||||
'hy-ypa-opa-kosu-porthania': [
|
||||
'oikeustieteellinen',
|
||||
'valtiotieteellinen',
|
||||
'kasvatustieteellinen',
|
||||
'humanistinen',
|
||||
'teologinen',
|
||||
'svenska',
|
||||
],
|
||||
'hy-ypa-opa-kosu-siltavuori': [
|
||||
'oikeustieteellinen',
|
||||
'valtiotieteellinen',
|
||||
'kasvatustieteellinen',
|
||||
'humanistinen',
|
||||
'teologinen',
|
||||
'svenska',
|
||||
],
|
||||
'hy-ypa-opa-kosu-viikki': [
|
||||
'bio- ja ympäristötieteellinen',
|
||||
'farmasia',
|
||||
'maatalous -metsätieteellinen',
|
||||
'eläinlääketieteellinen',
|
||||
],
|
||||
}
|
||||
|
||||
const doctoralIams = ['hy-tohtorikoulutus-johtoryhma', 'hy-tine']
|
||||
|
||||
const doctoralSchoolMap = {
|
||||
'hy-tutkijakoulut-hymy-jory': [
|
||||
'T920101',
|
||||
'T920102',
|
||||
'T920103',
|
||||
'T920104',
|
||||
'T920105',
|
||||
'T920106',
|
||||
'T920107',
|
||||
'T920108',
|
||||
'T920109',
|
||||
'T920110',
|
||||
'T920111',
|
||||
],
|
||||
'hy-tutkijakoulut-dshealth-jory': [
|
||||
'T921101',
|
||||
'T921102',
|
||||
'T921103',
|
||||
'T921104',
|
||||
'T921105',
|
||||
'T921106',
|
||||
'T921107',
|
||||
'T921108',
|
||||
'T921109',
|
||||
],
|
||||
'hy-tutkijakoulut-yeb-jory': ['T922101', 'T922102', 'T922103', 'T922104', 'T922105', 'T922106'],
|
||||
'hy-tutkijakoulut-donasci-jory': ['T923101', 'T923102', 'T923103', 'T923104', 'T923105', 'T923106', 'T923107'],
|
||||
}
|
||||
|
||||
const opetusVaradekaani = 'hy-varadekaanit-opetus'
|
||||
|
||||
const universityServices = 'hy-ypa-opa-dojo'
|
||||
|
||||
const dekaaniFacultyMap = {
|
||||
'hy-ttdk-dekanaatti': 'teologinen',
|
||||
'hy-oiktdk-dekanaatti': 'oikeustieteellinen',
|
||||
'hy-ltdk-dekanaatti': 'lääketieteellinen',
|
||||
'hy-humtdk-dekanaatti': 'humanistinen',
|
||||
'hy-mltdk-dekanaatti': 'matemaattis-luonnontieteellinen',
|
||||
'hy-ftdk-dekanaatti': 'farmasia',
|
||||
'hy-bytdk-dekanaatti': 'bio- ja ympäristötieteellinen',
|
||||
'hy-ktdk-dekanaatti': 'kasvatustieteellinen',
|
||||
'hy-valttdk-dekanaatti': 'valtiotieteellinen',
|
||||
'hy-sskh-rehtoraatti': 'svenska',
|
||||
'hy-mmtdk-dekanaatti': 'maatalous -metsätieteellinen',
|
||||
'hy-eltdk-dekanaatti': 'eläinlääketieteellinen',
|
||||
}
|
||||
|
||||
const oodikoneIams = [
|
||||
'grp-oodikone-staging-users',
|
||||
'grp-oodikone-basic-staging-users',
|
||||
'grp-oodikone-users',
|
||||
'grp-oodikone-basic-users',
|
||||
]
|
||||
|
||||
const universityWideGroups = ['hy-ypa-opa-opintoasiainpaallikot', 'hy-rehtoraatti']
|
||||
|
||||
const superAdminGroups = ['grp-toska']
|
||||
|
||||
const adminGroups = ['grp-ospa']
|
||||
|
||||
const employeeGroups = ['hy-employees']
|
||||
|
||||
const isSuperAdminIam = iam => superAdminGroups.includes(iam)
|
||||
|
||||
const isAdminIam = iam => adminGroups.includes(iam)
|
||||
|
||||
const isUniversityWideIam = iam => universityWideGroups.includes(iam)
|
||||
|
||||
const isDoctoralIam = iam => doctoralIams.includes(iam)
|
||||
|
||||
const isEmployeeIam = iam => employeeGroups.includes(iam)
|
||||
|
||||
const iamToDoctoralSchool = iam => doctoralSchoolMap[iam]
|
||||
|
||||
const getStudyLeaderGroup = iam => kojoMap[iam]
|
||||
|
||||
const iamToOrganisationCode = iam => {
|
||||
const organisationCodes = joryMap[iam]
|
||||
if (Array.isArray(organisationCodes)) {
|
||||
return organisationCodes
|
||||
}
|
||||
return [organisationCodes]
|
||||
}
|
||||
|
||||
const kosuIamToFaculties = iam => {
|
||||
const faculties = kosuFacultyMap[iam]
|
||||
if (!Array.isArray(faculties) || !faculties.length) return []
|
||||
|
||||
return faculties.map(f => facultyMap[f])
|
||||
}
|
||||
|
||||
const dekaaniIamToFaculty = iam => {
|
||||
const faculty = dekaaniFacultyMap[iam]
|
||||
if (!faculty) return null
|
||||
return facultyMap[faculty]
|
||||
}
|
||||
|
||||
const relevantIAMs = []
|
||||
.concat(Object.keys(joryMap))
|
||||
.concat(Object.keys(kojoMap))
|
||||
.concat(Object.values(kojoMap))
|
||||
.concat(Object.keys(kosuFacultyMap))
|
||||
.concat(Object.keys(dekaaniFacultyMap))
|
||||
.concat(opetusVaradekaani)
|
||||
.concat(universityServices)
|
||||
.concat(doctoralIams)
|
||||
.concat(Object.keys(doctoralSchoolMap))
|
||||
.concat(universityWideGroups)
|
||||
.concat(superAdminGroups)
|
||||
.concat(adminGroups)
|
||||
.concat(employeeGroups)
|
||||
.concat(oodikoneIams)
|
||||
|
||||
module.exports = {
|
||||
isSuperAdminIam,
|
||||
isAdminIam,
|
||||
isUniversityWideIam,
|
||||
isDoctoralIam,
|
||||
isEmployeeIam,
|
||||
iamToDoctoralSchool,
|
||||
iamToOrganisationCode,
|
||||
kosuIamToFaculties,
|
||||
dekaaniIamToFaculty,
|
||||
opetusVaradekaani,
|
||||
getStudyLeaderGroup,
|
||||
relevantIAMs,
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
const mapToDegreeCode = organisationCode => {
|
||||
if (!organisationCode) return ''
|
||||
if (organisationCode.length < 7) return ''
|
||||
const doctoral = organisationCode[0] === 'T'
|
||||
if (doctoral) {
|
||||
return organisationCode
|
||||
}
|
||||
|
||||
const [start, end] = organisationCode.split('-')
|
||||
if (end && end.length < 3) return ''
|
||||
if (start.length < 2) return ''
|
||||
const masters = end[0] === 'M'
|
||||
const code = `${masters ? 'M' : 'K'}H${start.substr(0, 2)}_${end.substr(-3)}`
|
||||
return code
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mapToDegreeCode,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,139 @@
|
||||
// Map programme codes to "letter indentifier"
|
||||
|
||||
const codes = {
|
||||
KH10_001: 'tuk',
|
||||
MH10_001: 'tum',
|
||||
KH20_001: 'on',
|
||||
MH20_001: 'otm',
|
||||
MH20_002: 'ibl',
|
||||
MH20_003: 'ggl',
|
||||
KH30_001: 'psyk',
|
||||
MH30_004: 'psym',
|
||||
KH30_002: 'logk',
|
||||
MH30_005: 'logm',
|
||||
MH30_001: 'll',
|
||||
MH30_002: 'tmed',
|
||||
MH30_003: 'hll',
|
||||
KH40_001: 'filk',
|
||||
KH40_002: 'ttk',
|
||||
KH40_003: 'kik',
|
||||
KH40_004: 'kok',
|
||||
KH40_005: 'kuka',
|
||||
KH40_006: 'hisk',
|
||||
MH40_001: 'ttm',
|
||||
MH40_002: 'kim',
|
||||
MH40_003: 'eng',
|
||||
MH40_004: 'rus',
|
||||
MH40_005: 'lingdig',
|
||||
MH40_006: 'tra',
|
||||
MH40_007: 'suku',
|
||||
MH40_008: 'nor',
|
||||
MH40_009: 'kir',
|
||||
MH40_010: 'kuma',
|
||||
MH40_011: 'ice',
|
||||
MH40_012: 'alku',
|
||||
MH40_014: 'spt',
|
||||
MH40_015: 'hism',
|
||||
KH50_001: 'mat',
|
||||
KH50_002: 'fys',
|
||||
KH50_003: 'kemk',
|
||||
KH50_004: 'mfkk',
|
||||
KH50_005: 'tkt',
|
||||
KH50_006: 'geok',
|
||||
KH50_007: 'maa',
|
||||
KH50_008: 'bsc',
|
||||
MH50_001: 'mast',
|
||||
MH50_002: 'lsi',
|
||||
MH50_003: 'tcm',
|
||||
MH50_004: 'paras',
|
||||
MH50_005: 'matres',
|
||||
MH50_006: 'atm',
|
||||
MH50_007: 'kem',
|
||||
MH50_008: 'mfkm',
|
||||
MH50_009: 'csm',
|
||||
MH50_010: 'data',
|
||||
MH50_011: 'geom',
|
||||
MH50_012: 'geog',
|
||||
MH50_013: 'usp',
|
||||
KH55_001: 'farm',
|
||||
MH55_001: 'prov',
|
||||
MH55_002: 'mpharm',
|
||||
KH57_001: 'bio',
|
||||
KH57_002: 'mole',
|
||||
KH57_003: 'env',
|
||||
MH57_001: 'eeb',
|
||||
MH57_002: 'ips',
|
||||
MH57_003: 'gmb',
|
||||
MH57_004: 'neuro',
|
||||
MH57_005: 'ecgs',
|
||||
KH60_001: 'eduk',
|
||||
MH60_001: 'edum',
|
||||
MH60_002: 'ce',
|
||||
KH70_001: 'pvk',
|
||||
KH70_002: 'yk',
|
||||
KH70_003: 'sosk',
|
||||
KH70_004: 'ecok',
|
||||
MH70_001: 'film',
|
||||
MH70_002: 'pvm',
|
||||
MH70_003: 'gpc',
|
||||
MH70_004: 'ym',
|
||||
MH70_005: 'cos',
|
||||
MH70_006: 'ens',
|
||||
MH70_007: 'msv',
|
||||
MH70_008: 'sosm',
|
||||
MH70_009: 'econ',
|
||||
MH70_011: 'sote',
|
||||
KH74_001: 'ksv',
|
||||
KH80_001: 'maatk',
|
||||
KH80_002: 'metsak',
|
||||
KH80_003: 'etk',
|
||||
KH80_004: 'yet',
|
||||
MH80_001: 'agri',
|
||||
MH80_002: 'agere',
|
||||
MH80_003: 'for',
|
||||
MH80_004: 'food',
|
||||
MH80_005: 'hnfb',
|
||||
MH80_006: 'ekm',
|
||||
MH80_007: 'mmb',
|
||||
KH90_001: 'elk',
|
||||
MH90_001: 'ell',
|
||||
T920101: 'dptheol',
|
||||
T920102: 'dplaw',
|
||||
T920111: 'philartsoc',
|
||||
T920103: 'dphistcult',
|
||||
T920104: 'dplang',
|
||||
T920105: 'sky',
|
||||
T920106: 'dpsocs',
|
||||
T920107: 'pyam',
|
||||
T920108: 'dpe',
|
||||
T920109: 'seduce',
|
||||
T920110: 'clic',
|
||||
T921101: 'dpbm',
|
||||
T921102: 'klto',
|
||||
T921103: 'docpop',
|
||||
T921104: 'findos',
|
||||
T921105: 'dpdr',
|
||||
T921106: 'ils',
|
||||
T921107: 'bandm',
|
||||
T921108: 'cvm',
|
||||
T921109: 'dphub',
|
||||
T922101: 'luova',
|
||||
T922102: 'dpps',
|
||||
T922103: 'denvi',
|
||||
T922104: 'agforee',
|
||||
T922105: 'mbdp',
|
||||
T922106: 'foodhealth',
|
||||
T923101: 'papu',
|
||||
T923102: 'geodoc',
|
||||
T923103: 'atm',
|
||||
T923104: 'chems',
|
||||
T923105: 'domast',
|
||||
T923106: 'matrena',
|
||||
T923107: 'docs',
|
||||
// manually added programs with no IAM, either abolished, or special co-operation programmes
|
||||
MH50_014: 'ech',
|
||||
MH40_013: 'mkk',
|
||||
MH70_010: 'imes',
|
||||
}
|
||||
|
||||
module.exports = { codes }
|
||||
Generated
+5437
-1447
File diff suppressed because it is too large
Load Diff
@@ -4,35 +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.18.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^5.1.1",
|
||||
"moment": "^2.29.1",
|
||||
"moment-timezone": "^0.5.33",
|
||||
"moment-timezone": "^0.5.37",
|
||||
"morgan": "^1.10.0",
|
||||
"pg": "^8.7.1",
|
||||
"pg": "^8.8.0",
|
||||
"redis": "^3.1.2",
|
||||
"sequelize": "^6.6.5",
|
||||
"sequelize": "^6.24.0",
|
||||
"umzug": "^2.3.0",
|
||||
"unfuck-utf8-headers-middleware": "^1.0.1",
|
||||
"uuid": "^3.4.0",
|
||||
"winston": "^3.3.3"
|
||||
"winston": "^3.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^1.19.4"
|
||||
|
||||
@@ -16,7 +16,7 @@ class DbConnection extends EventEmitter {
|
||||
pool: {
|
||||
max: 25,
|
||||
min: 0,
|
||||
acquire: 10000,
|
||||
acquire: 30000,
|
||||
idle: 300000000,
|
||||
},
|
||||
logging: false,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.dropTable('thesis_courses')
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
const { BIGINT, STRING, DATE, ARRAY } = require('sequelize')
|
||||
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.createTable('open_uni_population_searches', {
|
||||
id: {
|
||||
type: BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
user_id: {
|
||||
type: BIGINT,
|
||||
},
|
||||
name: {
|
||||
type: STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
course_codes: {
|
||||
type: ARRAY(STRING),
|
||||
},
|
||||
created_at: DATE,
|
||||
updated_at: DATE,
|
||||
})
|
||||
},
|
||||
down: () => {},
|
||||
}
|
||||
@@ -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 () => {},
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
const { ARRAY, STRING } = require('sequelize')
|
||||
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.addColumn('users', 'iam_groups', {
|
||||
type: ARRAY(STRING),
|
||||
allowNull: false,
|
||||
defaultValue: [],
|
||||
})
|
||||
},
|
||||
down: async queryInterface => {
|
||||
await queryInterface.removeColumn('users', 'iam_groups')
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
up: queryInterface => {
|
||||
return queryInterface.bulkInsert('access_groups', [
|
||||
{
|
||||
group_code: 'facultyStatistics',
|
||||
group_info: 'grants access to faculty statistics',
|
||||
},
|
||||
])
|
||||
},
|
||||
down: queryInterface => {
|
||||
return queryInterface.bulkDelete('access_groups', [
|
||||
{
|
||||
group_code: 'facultyStatistics',
|
||||
},
|
||||
])
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
up: queryInterface => {
|
||||
return queryInterface.bulkInsert('access_groups', [
|
||||
{
|
||||
group_code: 'openUniSearch',
|
||||
group_info: 'grants access to custom open uni course searches',
|
||||
},
|
||||
])
|
||||
},
|
||||
down: queryInterface => {
|
||||
return queryInterface.bulkDelete('access_groups', [
|
||||
{
|
||||
group_code: 'openUniSearch',
|
||||
},
|
||||
])
|
||||
},
|
||||
}
|
||||
@@ -1,18 +1,12 @@
|
||||
const { CronJob } = require('cron')
|
||||
const moment = require('moment')
|
||||
const {
|
||||
refreshProtoC,
|
||||
refreshStatus,
|
||||
refreshStatusGraduated,
|
||||
refreshUber,
|
||||
refreshProtoCProgramme,
|
||||
getStartYears,
|
||||
} = require('./services/trends')
|
||||
const { refreshStatus, refreshStatusGraduated, refreshUber, getStartYears } = 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 { faculties } = require('./services/organisations')
|
||||
const { updateFacultyOverview, updateFacultyProgressOverview } = require('./services/faculty/facultyUpdates')
|
||||
const { isProduction } = require('./conf-backend')
|
||||
const { getCurrentSemester } = require('./services/semesters')
|
||||
const logger = require('./util/logger')
|
||||
@@ -24,47 +18,41 @@ const refreshStudyrightAssociations = async () => {
|
||||
logger.info('Studyright associations refreshed!')
|
||||
}
|
||||
|
||||
const refreshOverview = async () => {
|
||||
logger.info('Refreshing throughput and productivity for programmes...')
|
||||
const codes = (await getAllProgrammes()).map(p => p.code).filter(code => code.includes('KH') || code.includes('MH'))
|
||||
const refreshFaculties = async () => {
|
||||
logger.info('Refreshing faculty overview statistics for all faculties')
|
||||
const facultyList = (await faculties()).filter(f => !['Y', 'H99', 'Y01', 'H92', 'H930'].includes(f.code))
|
||||
let ready = 0
|
||||
|
||||
for (const faculty of facultyList) {
|
||||
try {
|
||||
await updateFacultyOverview(faculty.code, 'ALL')
|
||||
await updateFacultyProgressOverview(faculty.code)
|
||||
ready += 1
|
||||
} catch (e) {
|
||||
logger.error({ message: `Failed to update stats for faculty ${faculty?.code} with type ALL`, meta: e })
|
||||
}
|
||||
logger.info(`${ready}/${facultyList.length} faculties done`)
|
||||
}
|
||||
logger.info('Faculty stats refreshed!')
|
||||
}
|
||||
|
||||
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') || /^(T)[0-9]{6}$/.test(code))
|
||||
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 () => {
|
||||
@@ -73,19 +61,6 @@ const refreshTeacherLeaderboard = async () => {
|
||||
await findAndSaveTeachers(currentSemestersYearCode - 1, currentSemestersYearCode)
|
||||
}
|
||||
|
||||
const refreshProtoCtoRedis = async () => {
|
||||
logger.info('Refreshing CDS ProtoC')
|
||||
const defaultQuery = { include_old_attainments: 'false', exclude_non_enrolled: 'false' }
|
||||
const onlyOld = { include_old_attainments: 'true', exclude_non_enrolled: 'false' }
|
||||
const onlyEnr = { include_old_attainments: 'false', exclude_non_enrolled: 'true' }
|
||||
const bothToggles = { include_old_attainments: 'true', exclude_non_enrolled: 'true' }
|
||||
await refreshProtoC(defaultQuery)
|
||||
await refreshProtoC(onlyOld)
|
||||
await refreshProtoC(onlyEnr)
|
||||
await refreshProtoC(bothToggles)
|
||||
logger.info('Refreshing CDS ProtoC doned')
|
||||
}
|
||||
|
||||
const refreshStatusToRedis = async () => {
|
||||
const unixMillis = moment().valueOf()
|
||||
const date = new Date(Number(unixMillis))
|
||||
@@ -122,32 +97,8 @@ const refreshUberToRedis = async () => {
|
||||
logger.info(`Refreshing CDS Uber data doned`)
|
||||
}
|
||||
|
||||
const refreshProtoCProgrammeToRedis = async () => {
|
||||
const codes = (await getAllProgrammes()).map(p => p.code).filter(code => code.includes('KH'))
|
||||
for (const code of codes) {
|
||||
logger.info(`Refreshing ProtoCProgramme code ${code}`)
|
||||
const defaultQuery = { include_old_attainments: 'false', exclude_non_enrolled: 'false', code }
|
||||
const onlyOld = { include_old_attainments: 'true', exclude_non_enrolled: 'false', code }
|
||||
const onlyEnr = { include_old_attainments: 'false', exclude_non_enrolled: 'true', code }
|
||||
const bothToggles = { include_old_attainments: 'true', exclude_non_enrolled: 'true', code }
|
||||
const queries = [defaultQuery, onlyOld, onlyEnr, bothToggles]
|
||||
for (const query of queries) {
|
||||
try {
|
||||
await refreshProtoCProgramme(query)
|
||||
} catch (e) {
|
||||
logger.error({
|
||||
message: `Error when refreshing ProtocProgramme code ${code}, query ${Object.keys({ query })[0]}`,
|
||||
meta: e,
|
||||
}) // object.keys is hax to print query variable name
|
||||
}
|
||||
}
|
||||
logger.info(`Refreshing ProtoCProgramme code ${code} doned`)
|
||||
}
|
||||
logger.info(`Refreshing protocprogramme doned`)
|
||||
}
|
||||
|
||||
const refreshStatistics = async () => {
|
||||
const statfuncs = [refreshStudyrightAssociations, refreshOverview, refreshTeacherLeaderboard]
|
||||
const statfuncs = [refreshStudyrightAssociations, refreshTeacherLeaderboard]
|
||||
logger.info('Refreshing statistics')
|
||||
for (const func of statfuncs) {
|
||||
await func()
|
||||
@@ -156,7 +107,7 @@ const refreshStatistics = async () => {
|
||||
}
|
||||
|
||||
const refreshTrends = async () => {
|
||||
const trendfuncs = [refreshProtoCtoRedis, refreshStatusToRedis, refreshUberToRedis, refreshProtoCProgrammeToRedis]
|
||||
const trendfuncs = [refreshStatusToRedis, refreshUberToRedis]
|
||||
logger.info('Refreshing trends')
|
||||
for (const func of trendfuncs) {
|
||||
await func()
|
||||
@@ -166,9 +117,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, refreshFaculties]) {
|
||||
await func()
|
||||
}
|
||||
})
|
||||
@@ -179,4 +131,5 @@ module.exports = {
|
||||
startCron,
|
||||
refreshStatistics,
|
||||
refreshTrends,
|
||||
refreshFaculties,
|
||||
}
|
||||
|
||||
@@ -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!).
|
||||
@@ -26,10 +27,16 @@ const accessLogger = morgan((tokens, req, res) => {
|
||||
'ms',
|
||||
].join(' ')
|
||||
|
||||
const usingIamRights = user.iamRights.some(programmeCode => tokens['url'](req, res).includes(programmeCode))
|
||||
const onlyIamRights = !user.isAdmin && user.rights.length === 0
|
||||
|
||||
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,
|
||||
// needed for Grafana IAM users panel
|
||||
isIamRights: onlyIamRights || usingIamRights,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
const roles = requiredRoles => async (req, res, next) => {
|
||||
if (req.user) {
|
||||
const { roles } = req
|
||||
const {
|
||||
user: { roles },
|
||||
} = req
|
||||
|
||||
if (requiredRoles.every(r => roles.indexOf(r) >= 0) || roles.includes('admin')) {
|
||||
return next()
|
||||
|
||||
@@ -1,56 +1,57 @@
|
||||
const Sentry = require('@sentry/node')
|
||||
const { ApplicationError } = require('../util/customErrors')
|
||||
const { getUser, getUserDataFor, getMockedUser } = require('../services/userService')
|
||||
const { getUser, getMockedUser } = require('../services/userService')
|
||||
const { requiredGroup } = require('../conf-backend')
|
||||
const _ = require('lodash')
|
||||
const { relevantIAMs } = require('../../config/IAMConfig')
|
||||
const { getOrganizationAccess } = require('../util/organizationAccess')
|
||||
|
||||
const parseIamGroups = iamGroups => iamGroups?.split(';').filter(Boolean) ?? []
|
||||
|
||||
const hasRequiredIamGroup = iamGroups => _.intersection(iamGroups, requiredGroup).length > 0
|
||||
const hasRequiredIamGroup = (iamGroups, iamRights) =>
|
||||
_.intersection(iamGroups, requiredGroup).length > 0 || iamRights.length > 0
|
||||
|
||||
const currentUserMiddleware = async (req, _, next) => {
|
||||
const { uid: username, 'shib-session-id': sessionId, shib_logout_url: logoutUrl } = req.headers
|
||||
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) {
|
||||
throw new ApplicationError('No session id found in request headers', 403, { logoutUrl })
|
||||
if (!sessionId || !username) {
|
||||
throw new ApplicationError('Not enough data in request headers', 403, { logoutUrl })
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
throw new ApplicationError('No username found in request headers', 403, { logoutUrl })
|
||||
}
|
||||
const { displayname: name, mail: email, hygroupcn: iamGroups, hypersonsisuid: sisId } = req.headers
|
||||
const parsedIamGroups = parseIamGroups(iamGroups)
|
||||
const iamGroups = parseIamGroups(hygroupcn).filter(iam => relevantIAMs.includes(iam))
|
||||
|
||||
req.user = await getUser({
|
||||
const iamRights = Object.keys(await getOrganizationAccess({ iamGroups }))
|
||||
|
||||
if (!hasRequiredIamGroup(iamGroups, iamRights)) {
|
||||
throw new ApplicationError('User does not have required iam group', 403, { logoutUrl })
|
||||
}
|
||||
|
||||
let user = await getUser({
|
||||
username,
|
||||
name,
|
||||
email,
|
||||
iamGroups: parsedIamGroups,
|
||||
iamGroups,
|
||||
sisId,
|
||||
})
|
||||
|
||||
if (!hasRequiredIamGroup(parsedIamGroups)) {
|
||||
throw new ApplicationError('User is not enabled', 403, { logoutUrl })
|
||||
if (showAsUser && user.isAdmin) {
|
||||
user = await getMockedUser({ userToMock: showAsUser, mockedBy: username })
|
||||
}
|
||||
|
||||
const showAsUser = req.headers['x-show-as-user']
|
||||
Sentry.setUser({ username: user.mockedBy ?? username })
|
||||
|
||||
if (showAsUser) {
|
||||
const mockedUser = await getMockedUser(username, showAsUser)
|
||||
if (mockedUser) {
|
||||
req.user = mockedUser
|
||||
}
|
||||
}
|
||||
req.user = user
|
||||
req.logoutUrl = logoutUrl
|
||||
|
||||
const { userId, mockedBy } = req.user
|
||||
|
||||
Sentry.setUser({ username: mockedBy ?? userId })
|
||||
|
||||
// TODO: remove this garbo
|
||||
const userData = await getUserDataFor(userId)
|
||||
Object.assign(req, userData)
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,10 @@ Course.init(
|
||||
course_unit_type: {
|
||||
type: STRING,
|
||||
},
|
||||
mainCourseCode: {
|
||||
field: 'main_course_code',
|
||||
type: STRING,
|
||||
},
|
||||
},
|
||||
{
|
||||
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 OpenUniPopulationSearch = sequelizeKone.define(
|
||||
'open_uni_population_searches',
|
||||
{
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT,
|
||||
autoIncrement: true,
|
||||
},
|
||||
userId: {
|
||||
type: Sequelize.BIGINT,
|
||||
},
|
||||
name: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
courseCodes: {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
},
|
||||
},
|
||||
{ underscored: true, timestamps: true }
|
||||
)
|
||||
|
||||
const StudyGuidanceGroupTag = sequelizeKone.define(
|
||||
'study_guidance_group_tags',
|
||||
{
|
||||
@@ -173,10 +181,10 @@ module.exports = {
|
||||
MandatoryCourse,
|
||||
TagStudent,
|
||||
Tag,
|
||||
ThesisCourse,
|
||||
ThesisTypeEnums,
|
||||
MandatoryCourseLabels,
|
||||
CustomPopulationSearch,
|
||||
OpenUniPopulationSearch,
|
||||
sequelizeKone,
|
||||
ExcludedCourse,
|
||||
StudyGuidanceGroupTag,
|
||||
|
||||
@@ -76,6 +76,11 @@ const User = sequelizeUser.define(
|
||||
email: { type: Sequelize.STRING },
|
||||
language: { type: Sequelize.STRING },
|
||||
sisu_person_id: { type: Sequelize.STRING },
|
||||
iam_groups: {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
allowNull: false,
|
||||
defaultValue: [],
|
||||
},
|
||||
last_login: { type: Sequelize.DATE },
|
||||
},
|
||||
{
|
||||
@@ -83,23 +88,6 @@ const User = sequelizeUser.define(
|
||||
timestamps: false,
|
||||
}
|
||||
)
|
||||
const HyGroup = sequelizeUser.define('hy_group', {
|
||||
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)
|
||||
@@ -110,16 +98,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.hasMany(UserFaculties, { as: 'faculty', foreignKey: 'userId' })
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
UserElementDetails,
|
||||
AccessGroup,
|
||||
HyGroup,
|
||||
UserFaculties,
|
||||
sequelizeUser,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { Model, STRING, DATE, INTEGER } = require('sequelize')
|
||||
const { Model, STRING, DATE, INTEGER, BOOLEAN } = require('sequelize')
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
class Student extends Model {}
|
||||
@@ -16,6 +16,7 @@ Student.init(
|
||||
creditcount: { type: INTEGER },
|
||||
dateofuniversityenrollment: { type: DATE },
|
||||
email: { type: STRING },
|
||||
secondary_email: { type: STRING },
|
||||
national_student_number: { type: STRING },
|
||||
country_fi: { type: STRING },
|
||||
country_sv: { type: STRING },
|
||||
@@ -25,6 +26,7 @@ Student.init(
|
||||
home_country_en: { type: STRING },
|
||||
gender_code: { type: INTEGER },
|
||||
sis_person_id: { type: STRING },
|
||||
dissemination_info_allowed: { type: BOOLEAN },
|
||||
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: STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
studentnumber: {
|
||||
type: STRING,
|
||||
references: {
|
||||
model: 'student',
|
||||
key: 'studentnumber',
|
||||
},
|
||||
onUpdate: 'cascade',
|
||||
onDelete: 'cascade',
|
||||
allowNull: false,
|
||||
},
|
||||
programme_code: {
|
||||
type: STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
included_courses: {
|
||||
type: ARRAY(STRING),
|
||||
},
|
||||
completed_credits: {
|
||||
type: INTEGER,
|
||||
},
|
||||
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))
|
||||
@@ -30,6 +30,7 @@ const trends = require('./routes/trends')
|
||||
const programmeModules = require('./routes/programmeModules')
|
||||
const studyGuidanceGroups = require('./routes/studyGuidanceGroups')
|
||||
const studyProgramme = require('./routes/studyProgramme')
|
||||
const customOpenUniSearch = require('./routes/customOpenUniSearch')
|
||||
const initializeSentry = require('./util/sentry')
|
||||
|
||||
const errorMiddleware = require('./middleware/errorMiddleware')
|
||||
@@ -57,7 +58,8 @@ module.exports = (app, url) => {
|
||||
app.use(url, tags)
|
||||
app.use(url, programmeModules)
|
||||
app.use(url, studyProgramme)
|
||||
app.use(`${url}/faculties`, faculties)
|
||||
app.use(`${url}/openunisearch`, auth.roles(['openUniSearch']), customOpenUniSearch)
|
||||
app.use(`${url}/faculties`, auth.roles(['facultyStatistics']), faculties)
|
||||
app.use(`${url}/updater`, auth.roles(['admin']), updater)
|
||||
app.use(`${url}/teachers`, auth.roles(['teachers']), teachers)
|
||||
app.use(`${url}/users`, auth.roles(['admin']), users)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const router = require('express').Router()
|
||||
const crypto = require('crypto')
|
||||
const Course = require('../services/courses')
|
||||
const { faculties } = require('../services/organisations')
|
||||
const { validateParamLength } = require('../util')
|
||||
|
||||
router.get('/v2/coursesmulti', async (req, res) => {
|
||||
@@ -16,7 +17,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))
|
||||
|
||||
@@ -26,10 +29,9 @@ router.get('/v3/courseyearlystats', async (req, res) => {
|
||||
return res.status(403).json({ error: 'No programmes so no access to course stats' })
|
||||
}
|
||||
|
||||
const { codes, separate: sep, unifyOpenUniCourses: unify } = req.query
|
||||
const { codes, separate: sep } = req.query
|
||||
|
||||
const separate = !sep ? false : JSON.parse(sep)
|
||||
const unifyOpenUniCourses = !unify ? false : JSON.parse(unify)
|
||||
if (!codes) {
|
||||
res.status(422).send('Missing required query parameters')
|
||||
} else {
|
||||
@@ -37,9 +39,14 @@ router.get('/v3/courseyearlystats', async (req, res) => {
|
||||
// and users with rights to any specific study programmes
|
||||
const anonymize = allowedRoles !== 'admin' && rights.length < 1
|
||||
const anonymizationSalt = anonymize ? crypto.randomBytes(12).toString('hex') : null
|
||||
const results = await Course.courseYearlyStats(codes, separate, unifyOpenUniCourses, anonymizationSalt)
|
||||
const results = await Course.courseYearlyStats(codes, separate, anonymizationSalt)
|
||||
res.json(results)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/faculties-only', async (_req, res) => {
|
||||
const facultyList = await faculties()
|
||||
res.json(facultyList)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
const router = require('express').Router()
|
||||
var moment = require('moment-timezone')
|
||||
const { getCustomOpenUniCourses } = require('../services/openUni/openUniStats')
|
||||
const {
|
||||
getOpenUniSearches,
|
||||
createNewSearch,
|
||||
deleteSearch,
|
||||
updateSearch,
|
||||
} = require('../services/openUni/openUniManageSearches')
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const courseCodes = JSON.parse(req.query?.courselist) || []
|
||||
const startdate = req.query?.startdate ? req.query?.startdate : moment('01-08-2017 00:00:00', 'DD-MM-YYYY')
|
||||
const enddate = req.query?.enddate ? req.query?.enddate : moment().endOf('day')
|
||||
|
||||
if (!Array.isArray(courseCodes)) return res.status(400).json({ error: 'Courses must be of type array' })
|
||||
const customOpenUniSearches = await getCustomOpenUniCourses(courseCodes, startdate, enddate)
|
||||
return res.json(customOpenUniSearches)
|
||||
})
|
||||
|
||||
router.get('/searches', async (req, res) => {
|
||||
const userId = req.user.id
|
||||
const foundSearches = await getOpenUniSearches(userId)
|
||||
return res.json(foundSearches)
|
||||
})
|
||||
|
||||
router.post('/searches', async (req, res) => {
|
||||
const courseCodes = req.body?.courselist || []
|
||||
const userId = req.user.id
|
||||
const name = req.body?.name
|
||||
|
||||
if (!name) return res.status(400).json({ error: 'Name missing' })
|
||||
if (courseCodes && !Array.isArray(courseCodes))
|
||||
return res.status(400).json({ error: 'Course codes must be type of array' })
|
||||
|
||||
const createdSearch = await createNewSearch(userId, name, courseCodes)
|
||||
if (!createdSearch) return res.status(400).json({ error: '' })
|
||||
return res.status(201).json({
|
||||
id: createdSearch.id,
|
||||
userId: createdSearch.userId,
|
||||
name: createdSearch.name,
|
||||
courseList: createdSearch.courseCodes,
|
||||
updatedAt: createdSearch.updatedAt,
|
||||
})
|
||||
})
|
||||
|
||||
router.put('/searches/:id', async (req, res) => {
|
||||
const id = req.params?.id
|
||||
const courseCodes = req.body?.courselist || []
|
||||
const userId = req.user.id
|
||||
if (!id || !userId) return res.status(422).end()
|
||||
const updatedSearch = await updateSearch(userId, id, courseCodes)
|
||||
|
||||
if (!updatedSearch) return res.status(404).json({ error: `Open uni search could not be found` })
|
||||
return res.json({
|
||||
id: updatedSearch.id,
|
||||
userId: updatedSearch.userId,
|
||||
name: updatedSearch.name,
|
||||
courseList: updatedSearch.courseCodes,
|
||||
updatedAt: updatedSearch.updatedAt,
|
||||
})
|
||||
})
|
||||
|
||||
router.delete('/searches/:id', async (req, res) => {
|
||||
const id = req.params?.id
|
||||
const userId = req.user.id
|
||||
if (!id || !userId) return res.status(422).end()
|
||||
|
||||
const deletedSearch = await deleteSearch(userId, id)
|
||||
if (!deletedSearch) return res.status(404).json({ error: `Open uni search could not be found` })
|
||||
return res.json(id)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -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,138 +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()
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -1,9 +1,206 @@
|
||||
const router = require('express').Router()
|
||||
const { faculties } = require('../services/organisations')
|
||||
const { combineFacultyBasics } = require('../services/faculty/facultyBasics')
|
||||
const { combineFacultyCredits } = require('../services/faculty/facultyCredits')
|
||||
const { findFacultyProgrammeCodes } = require('../services/faculty/faculty')
|
||||
const { combineFacultyThesisWriters } = require('../services/faculty/facultyThesisWriters')
|
||||
const { countGraduationTimes } = require('../services/faculty/facultyGraduationTimes')
|
||||
const { updateFacultyOverview, updateFacultyProgressOverview } = require('../services/faculty/facultyUpdates')
|
||||
const { combineFacultyStudentProgress } = require('../services/faculty/facultyStudentProgress')
|
||||
const { combineFacultyStudents } = require('../services/faculty/facultyStudents')
|
||||
|
||||
const {
|
||||
getFacultyProgrammes,
|
||||
setFacultyProgrammes,
|
||||
getBasicStats,
|
||||
setBasicStats,
|
||||
getCreditStats,
|
||||
setCreditStats,
|
||||
getThesisWritersStats,
|
||||
setThesisWritersStats,
|
||||
getGraduationStats,
|
||||
setGraduationStats,
|
||||
getFacultyStudentStats,
|
||||
setFacultyStudentStats,
|
||||
getFacultyProgressStats,
|
||||
setFacultyProgressStats,
|
||||
} = require('../services/faculty/facultyService')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
// Faculty uses a lot of tools designed for Study programme.
|
||||
// Some of them have been copied here and slightly edited for faculty purpose.
|
||||
|
||||
const getProgrammes = async (code, programmeFilter) => {
|
||||
const programmes = await getFacultyProgrammes(code, programmeFilter)
|
||||
if (programmes) return programmes
|
||||
let updatedProgrammes = await findFacultyProgrammeCodes(code, programmeFilter)
|
||||
if (updatedProgrammes) updatedProgrammes = await setFacultyProgrammes(code, updatedProgrammes, programmeFilter)
|
||||
|
||||
return updatedProgrammes
|
||||
}
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const facultyList = await faculties()
|
||||
const ignore = ['Y', 'H99', 'Y01', 'H92', 'H930']
|
||||
const facultyList = (await faculties()).filter(f => !ignore.includes(f.code))
|
||||
facultyList.sort((a, b) => (a.name.fi > b.name.fi ? 1 : -1))
|
||||
res.json(facultyList)
|
||||
})
|
||||
|
||||
router.get('/:id/basicstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const yearType = req.query?.year_type
|
||||
const programmeFilter = req.query?.programme_filter
|
||||
const specialGroups = req.query?.special_groups
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
const data = await getBasicStats(code, yearType, programmeFilter, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
const wantedProgrammes = await getProgrammes(code, programmeFilter)
|
||||
if (!wantedProgrammes) return res.status(422).end()
|
||||
|
||||
//all programmes are required for correct sorting of transfers
|
||||
let allProgrammeCodes = []
|
||||
if (programmeFilter === 'NEW_STUDY_PROGRAMMES') {
|
||||
const allProgs = await getProgrammes(code, 'ALL_PROGRAMMES')
|
||||
allProgs?.data.forEach(prog => allProgrammeCodes.push(prog.code))
|
||||
} else {
|
||||
wantedProgrammes?.data.forEach(prog => allProgrammeCodes.push(prog.code))
|
||||
}
|
||||
|
||||
let updatedStats = await combineFacultyBasics(
|
||||
code,
|
||||
wantedProgrammes.data,
|
||||
yearType,
|
||||
allProgrammeCodes,
|
||||
programmeFilter,
|
||||
specialGroups
|
||||
)
|
||||
if (updatedStats) {
|
||||
updatedStats = await setBasicStats(updatedStats, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
return res.json(updatedStats)
|
||||
})
|
||||
|
||||
router.get('/:id/creditstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const yearType = req.query?.year_type
|
||||
const specialGroups = req.query?.special_groups
|
||||
const programmeFilter = req.query?.programme_filter
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
const data = await getCreditStats(code, yearType, programmeFilter, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
|
||||
const programmes = await getProgrammes(code, programmeFilter)
|
||||
if (!programmes) return res.status(422).end()
|
||||
|
||||
// list of all programmes is also needed in classification of credits
|
||||
const allProgrammes = programmeFilter === 'ALL_PROGRAMMES' ? programmes : await getProgrammes(code, 'ALL_PROGRAMMES')
|
||||
|
||||
let updatedStats = await combineFacultyCredits(code, programmes.data, allProgrammes.data, yearType, specialGroups)
|
||||
if (updatedStats) {
|
||||
updatedStats = await setCreditStats(updatedStats, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
|
||||
return res.json(updatedStats)
|
||||
})
|
||||
|
||||
router.get('/:id/thesisstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const yearType = req.query?.year_type
|
||||
const specialGroups = req.query?.special_groups
|
||||
const programmeFilter = req.query?.programme_filter
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
const data = await getThesisWritersStats(code, yearType, programmeFilter, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
const programmes = await getProgrammes(code, programmeFilter)
|
||||
if (!programmes) return res.status(422).end()
|
||||
let updateStats = await combineFacultyThesisWriters(code, programmes.data, yearType, specialGroups)
|
||||
if (updateStats) {
|
||||
updateStats = await setThesisWritersStats(updateStats, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
return res.json(updateStats)
|
||||
})
|
||||
|
||||
router.get('/:id/graduationtimes', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const programmeFilter = req.query?.programme_filter
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
const data = await getGraduationStats(code, programmeFilter)
|
||||
if (data) {
|
||||
return res.json(data)
|
||||
}
|
||||
let updatedStats = await countGraduationTimes(code, programmeFilter)
|
||||
if (updatedStats) {
|
||||
updatedStats = await setGraduationStats(updatedStats, programmeFilter)
|
||||
}
|
||||
return res.json(updatedStats)
|
||||
})
|
||||
|
||||
router.get('/:id/progressstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const programmeFilter = req.query.programme_filter
|
||||
const specialGroups = req.query?.special_groups
|
||||
const graduated = req.query?.graduated
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
const data = await getFacultyProgressStats(code, specialGroups, graduated)
|
||||
if (data) return res.json(data)
|
||||
const programmes = await getProgrammes(code, programmeFilter)
|
||||
if (!programmes) return res.status(422).end()
|
||||
let updateStats = await combineFacultyStudentProgress(code, programmes.data, specialGroups, graduated)
|
||||
if (updateStats) {
|
||||
updateStats = await setFacultyProgressStats(updateStats, specialGroups, graduated)
|
||||
}
|
||||
return res.json(updateStats)
|
||||
})
|
||||
|
||||
router.get('/:id/studentstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const programmeFilter = req.query.programme_filter
|
||||
const specialGroups = req.query?.special_groups
|
||||
const graduated = req.query?.graduated
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
const data = await getFacultyStudentStats(code, specialGroups, graduated)
|
||||
if (data) return res.json(data)
|
||||
const programmes = await getProgrammes(code, programmeFilter)
|
||||
if (!programmes) return res.status(422).end()
|
||||
let updateStats = await combineFacultyStudents(code, programmes.data, specialGroups, graduated)
|
||||
if (updateStats) {
|
||||
updateStats = await setFacultyStudentStats(updateStats, specialGroups, graduated)
|
||||
}
|
||||
return res.json(updateStats)
|
||||
})
|
||||
|
||||
router.get('/:id/update_basicview', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const statsType = req.query?.stats_type
|
||||
if (code) {
|
||||
let result = null
|
||||
try {
|
||||
result = await updateFacultyOverview(code, statsType)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update faculty ${code} basic tab stats for ${statsType}: ${e}`)
|
||||
}
|
||||
return res.json(result)
|
||||
}
|
||||
return res.status(422).end()
|
||||
})
|
||||
|
||||
router.get('/:id/update_progressview', async (req, res) => {
|
||||
const code = req.params.id
|
||||
if (code) {
|
||||
let result = null
|
||||
try {
|
||||
result = await updateFacultyProgressOverview(code)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update faculty ${code} progress tab stats: ${e}`)
|
||||
}
|
||||
return res.json(result)
|
||||
}
|
||||
return res.status(422).end()
|
||||
})
|
||||
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 {
|
||||
user: { 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
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
const router = require('express').Router()
|
||||
const { ApplicationError } = require('../util/customErrors')
|
||||
const _ = require('lodash')
|
||||
|
||||
router.get('/login', async (req, res) => {
|
||||
const { user: userFromReq, logoutUrl } = req
|
||||
const { user, logoutUrl } = req
|
||||
|
||||
if (!userFromReq) {
|
||||
if (!user) {
|
||||
throw new ApplicationError('User not found', 404)
|
||||
}
|
||||
const roles = userFromReq.roles.map(role => role.group_code)
|
||||
const user = {
|
||||
...userFromReq,
|
||||
roles,
|
||||
isAdmin: roles.includes('admin'),
|
||||
}
|
||||
|
||||
res.send({
|
||||
user,
|
||||
// don't send possibly huge list of students to frontend
|
||||
user: _.omit(user, ['studentsUserCanAccess']),
|
||||
logoutUrl,
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
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 { mapToProviders } = require('../util/utils')
|
||||
const { encrypt, decrypt } = require('../services/encrypt')
|
||||
const { mapCodesToIds } = require('../services/studyprogrammeHelpers')
|
||||
|
||||
const { ApplicationError } = require('../util/customErrors')
|
||||
|
||||
const filterPersonalTags = (population, userId) => {
|
||||
return {
|
||||
@@ -37,16 +40,26 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
|
||||
req.body.months = 12
|
||||
}
|
||||
|
||||
const encrypted = req.body.selectedStudents[0]?.encryptedData
|
||||
|
||||
if (req.body.years) {
|
||||
const upperYearBound = new Date().getFullYear() + 1
|
||||
const multicoursestatPromises = Promise.all(
|
||||
req.body.years.map(year => {
|
||||
if (req.body.selectedStudentsByYear) {
|
||||
req.body.selectedStudents = req.body.selectedStudentsByYear[year]
|
||||
req.body.selectedStudents = encrypted
|
||||
? req.body.selectedStudents
|
||||
.filter(
|
||||
({ encryptedData }) =>
|
||||
req.body.selectedStudentsByYear[year] &&
|
||||
req.body.selectedStudentsByYear[year].includes(encryptedData)
|
||||
)
|
||||
.map(decrypt)
|
||||
: req.body.selectedStudentsByYear[year]
|
||||
}
|
||||
const newMonths = (upperYearBound - Number(year)) * 12
|
||||
const query = { ...req.body, year, months: newMonths }
|
||||
const coursestatistics = Population.bottlenecksOf(query)
|
||||
const coursestatistics = Population.bottlenecksOf(query, null, encrypted)
|
||||
return coursestatistics
|
||||
})
|
||||
)
|
||||
@@ -59,7 +72,9 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
|
||||
|
||||
res.json(result)
|
||||
} else {
|
||||
const result = await Population.bottlenecksOf(req.body)
|
||||
if (encrypted) req.body.selectedStudents = req.body.selectedStudents.map(decrypt)
|
||||
|
||||
const result = await Population.bottlenecksOf(req.body, null, encrypted)
|
||||
|
||||
if (result.error) {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
@@ -67,6 +82,20 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
|
||||
return
|
||||
}
|
||||
|
||||
//These cause problems in front (where they were originally calculated)
|
||||
// Do them here for the moment
|
||||
for (const course of result.coursestatistics) {
|
||||
// course.students.notParticipated = {}
|
||||
course.students.didNotPass = {}
|
||||
|
||||
for (const student of Object.keys(course.students.all)) {
|
||||
if (
|
||||
!Object.keys(course.students.passed).includes(student) &&
|
||||
!Object.keys(course.students.retryPassed).includes(student)
|
||||
)
|
||||
course.students.didNotPass[student] = true
|
||||
}
|
||||
}
|
||||
res.json(result)
|
||||
}
|
||||
})
|
||||
@@ -80,20 +109,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 {
|
||||
user: { 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 +142,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 {
|
||||
user: { 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,33 +169,24 @@ 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 {
|
||||
user: { userId, sisPersonId },
|
||||
roles,
|
||||
user: { isAdmin, studentsUserCanAccess },
|
||||
body: { studentnumberlist: studentnumbersInReq, studentCountLimit, ...query },
|
||||
} = 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,
|
||||
studyRights: [],
|
||||
semesters: ['FALL', 'SPRING'],
|
||||
months: 10000,
|
||||
year: query?.year ?? 1900,
|
||||
studyRights: query?.studyRights ?? [],
|
||||
semesters: query?.semesters ?? ['FALL', 'SPRING'],
|
||||
months: query?.months ?? 10000,
|
||||
studentCountLimit,
|
||||
},
|
||||
studentnumberlist
|
||||
)
|
||||
@@ -202,15 +208,15 @@ router.get('/v3/populationstatistics', async (req, res) => {
|
||||
return
|
||||
}
|
||||
let studyRights = null
|
||||
try {
|
||||
studyRights = JSON.parse(studyRightsJSON)
|
||||
const { rights, roles } = req
|
||||
studyRights = JSON.parse(studyRightsJSON)
|
||||
const {
|
||||
user: { rights, iamRights, isAdmin },
|
||||
} = req
|
||||
|
||||
if (!roles || !roles.includes('admin')) {
|
||||
if (!rights.includes(studyRights.programme)) {
|
||||
res.status(403).json([])
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (!isAdmin && !rights.includes(studyRights.programme) && !iamRights.includes(studyRights.programme)) {
|
||||
res.status(403).json([])
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(400).json({ error: 'The query had invalid studyRights' })
|
||||
@@ -254,32 +260,47 @@ router.get('/v3/populationstatistics', async (req, res) => {
|
||||
return
|
||||
}
|
||||
|
||||
// Obfuscate if user has only iam rights
|
||||
if (!isAdmin && !rights.includes(studyRights.programme)) {
|
||||
result.students = result.students.map(student => {
|
||||
const { iv, encryptedData } = encrypt(student.studentNumber)
|
||||
const obfuscatedBirthDate = new Date(new Date(student.birthdate).setMonth(0, 0)) // only birth year for age distribution
|
||||
return {
|
||||
...student,
|
||||
firstnames: '',
|
||||
lastname: '',
|
||||
started: null,
|
||||
iv,
|
||||
studentNumber: encryptedData,
|
||||
name: '',
|
||||
email: '',
|
||||
tags: [],
|
||||
birthdate: obfuscatedBirthDate,
|
||||
obfuscated: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
res.json(filterPersonalTags(result, user.id))
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/v3/populationstatisticsbytag', async (req, res) => {
|
||||
const { tag, studyRights: studyRightsJSON, months, year } = req.query
|
||||
const { user } = 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 {
|
||||
user: { 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 +322,14 @@ router.get('/v3/populationstatisticsbytag', async (req, res) => {
|
||||
return
|
||||
}
|
||||
|
||||
res.json(filterPersonalTags(result, user.id))
|
||||
res.json(filterPersonalTags(result, id))
|
||||
})
|
||||
|
||||
router.get('/v3/populationstatisticsbycourse', async (req, res) => {
|
||||
const { coursecodes, from, to, separate: sep } = req.query
|
||||
const { user, roles } = req
|
||||
const { coursecodes, from, to, separate: sep, unifyCourses } = req.query
|
||||
const {
|
||||
user: { id, isAdmin, studentsUserCanAccess: allStudentsUserCanAccess, rights },
|
||||
} = req
|
||||
const separate = sep ? JSON.parse(sep) : false
|
||||
|
||||
if (!coursecodes || !from || !to) {
|
||||
@@ -322,7 +345,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.
|
||||
@@ -334,15 +363,15 @@ 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(user.userId)
|
||||
const codes = unitsUserCanAccess.map(unit => unit.id)
|
||||
studentsUserCanAccess = new Set(await Student.filterStudentnumbersByAccessrights(studentnumbers, codes))
|
||||
let courseproviders = []
|
||||
if (!isAdmin) {
|
||||
courseproviders = await CourseService.getCourseProvidersForCourses(JSON.parse(coursecodes))
|
||||
}
|
||||
const rightsMappedToProviders = mapToProviders(rights)
|
||||
const found = courseproviders.some(r => rightsMappedToProviders.includes(r))
|
||||
|
||||
const studentsUserCanAccess =
|
||||
isAdmin || found ? new Set(studentnumbers) : new Set(_.intersection(studentnumbers, allStudentsUserCanAccess))
|
||||
|
||||
const randomHash = crypto.randomBytes(12).toString('hex')
|
||||
const obfuscateStudent = ({ studyrights, studentNumber, courses, gender_code }) => ({
|
||||
@@ -368,24 +397,16 @@ router.get('/v3/populationstatisticsbycourse', async (req, res) => {
|
||||
return
|
||||
}
|
||||
|
||||
res.json(filterPersonalTags(result, user.id))
|
||||
res.json(filterPersonalTags(result, id))
|
||||
})
|
||||
|
||||
router.post('/v3/populationstatisticsbystudentnumbers', async (req, res) => {
|
||||
const { studentnumberlist, usingStudyGuidanceGroups } = req.body
|
||||
const { roles, user } = req
|
||||
const { studentnumberlist } = req.body
|
||||
const {
|
||||
user: { isAdmin, id, studentsUserCanAccess },
|
||||
} = req
|
||||
|
||||
if (usingStudyGuidanceGroups) {
|
||||
if (!user.sisPersonId) {
|
||||
logger.error(`User ${user.userId} tried to get person groups but personId was ${user.sisPersonId} in header`)
|
||||
return res.status(400).json({ error: 'Not possible to get groups without personId' })
|
||||
}
|
||||
}
|
||||
|
||||
const studentsUserCanAccess = usingStudyGuidanceGroups
|
||||
? await getAllStudentsUserHasInGroups(user.sisPersonId)
|
||||
: await UserService.getStudentsUserCanAccess(studentnumberlist, roles, user.userId)
|
||||
const filteredStudentNumbers = studentnumberlist.filter(s => studentsUserCanAccess.has(s))
|
||||
const filteredStudentNumbers = isAdmin ? studentnumberlist : _.intersection(studentnumberlist, studentsUserCanAccess)
|
||||
|
||||
const result = await Population.optimizedStatisticsOf(
|
||||
{
|
||||
@@ -400,16 +421,20 @@ router.post('/v3/populationstatisticsbystudentnumbers', async (req, res) => {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
return
|
||||
}
|
||||
res.status(200).json(filterPersonalTags(result, user.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, iamRights, roles },
|
||||
} = req
|
||||
if (roles?.includes('admin')) {
|
||||
const studyrights = await StudyrightService.getAssociations()
|
||||
mapCodesToIds(studyrights.programmes)
|
||||
res.json(studyrights)
|
||||
} else {
|
||||
const studyrights = await StudyrightService.getFilteredAssociations(rights)
|
||||
const studyrights = await StudyrightService.getFilteredAssociations(rights.concat(iamRights))
|
||||
mapCodesToIds(studyrights.programmes)
|
||||
res.json(studyrights)
|
||||
}
|
||||
})
|
||||
@@ -421,8 +446,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,
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
const router = require('express').Router()
|
||||
const Student = require('../services/students')
|
||||
const userService = require('../services/userService')
|
||||
const Unit = require('../services/units')
|
||||
const { getAllStudentsUserHasInGroups } = require('../services/studyGuidanceGroups')
|
||||
const { ApplicationError } = require('../util/customErrors')
|
||||
|
||||
const filterStudentTags = (student, userId) => {
|
||||
return {
|
||||
@@ -13,8 +11,7 @@ const filterStudentTags = (student, userId) => {
|
||||
|
||||
router.get('/students', async (req, res) => {
|
||||
const {
|
||||
roles,
|
||||
user: { userId, sisPersonId },
|
||||
user: { isAdmin, studentsUserCanAccess },
|
||||
query: { searchTerm },
|
||||
} = req
|
||||
|
||||
@@ -26,66 +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)
|
||||
let extraStudents = []
|
||||
if (roles.includes('studyGuidanceGroups')) {
|
||||
const matchingOnlyByTerm = await Student.bySearchTerm(trimmedSearchTerm)
|
||||
const studentsUserCanAccess = await getAllStudentsUserHasInGroups(sisPersonId)
|
||||
extraStudents.push(...matchingOnlyByTerm.filter(s => studentsUserCanAccess.has(s.studentNumber)))
|
||||
}
|
||||
const matchingStudents = await Student.bySearchTermAndElements(trimmedSearchTerm, codes)
|
||||
res.json([...matchingStudents, ...extraStudents])
|
||||
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, user } = req
|
||||
const {
|
||||
user: { id, isAdmin, studentsUserCanAccess },
|
||||
} = req
|
||||
|
||||
if (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, user.id)).end()
|
||||
if (!isAdmin && !studentsUserCanAccess.includes(studentId)) {
|
||||
throw new ApplicationError('Error finding student', 400)
|
||||
}
|
||||
|
||||
const uid = req.user.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 (roles?.includes('studyGuidanceGroups')) {
|
||||
const studentsUserCanAccess = await getAllStudentsUserHasInGroups(user.sisPersonId)
|
||||
if (studentsUserCanAccess.has(studentId)) {
|
||||
return res.status(200).json(filterStudentTags(student, user.id)).end()
|
||||
}
|
||||
}
|
||||
|
||||
if (rights.some(right => right !== null)) {
|
||||
res.status(200).json(filterStudentTags(student, user.id)).end()
|
||||
} else {
|
||||
res.status(400).json({ error: 'error finding student' }).end()
|
||||
}
|
||||
res.json(filterStudentTags(student, id))
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
const router = require('express').Router()
|
||||
const {
|
||||
getBasicStatsForStudytrack,
|
||||
getCreditStatsForStudytrack,
|
||||
getGraduationStatsForStudytrack,
|
||||
} = require('../services/studyprogrammeStats')
|
||||
const { getBasicStatsForStudytrack } = require('../services/studyprogrammeBasics')
|
||||
const { getCreditStatsForStudytrack } = require('../services/studyprogrammeCredits')
|
||||
const { getGraduationStatsForStudytrack } = require('../services/studyprogrammeGraduations')
|
||||
const { getStudytrackStatsForStudyprogramme } = require('../services/studytrackStats')
|
||||
|
||||
const { getStudyprogrammeCoursesForStudytrack } = require('../services/studyprogrammeCourses')
|
||||
const {
|
||||
getBasicStats,
|
||||
setBasicStats,
|
||||
@@ -16,111 +14,130 @@ const {
|
||||
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)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get code ${code} basic stats: ${e}`)
|
||||
}
|
||||
if (!data) {
|
||||
try {
|
||||
const result = await getBasicStatsForStudytrack({
|
||||
studyprogramme: req.params.id,
|
||||
yearType,
|
||||
})
|
||||
data = await setBasicStats(result, yearType)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} basic stats: ${e}`)
|
||||
}
|
||||
}
|
||||
return res.json(data)
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
const data = await getBasicStats(code, yearType, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
|
||||
const updated = await getBasicStatsForStudytrack({
|
||||
studyprogramme: req.params.id,
|
||||
settings: {
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
})
|
||||
if (updated) await setBasicStats(updated, yearType, specialGroups)
|
||||
return res.json(updated)
|
||||
})
|
||||
|
||||
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)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get code ${code} credit stats`)
|
||||
}
|
||||
if (!data) {
|
||||
try {
|
||||
const result = await getCreditStatsForStudytrack({
|
||||
studyprogramme: req.params.id,
|
||||
yearType,
|
||||
})
|
||||
data = await setCreditStats(result, yearType)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} credit stats`)
|
||||
}
|
||||
}
|
||||
return res.json(data)
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
const data = await getCreditStats(code, yearType, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
const updatedStats = await getCreditStatsForStudytrack({
|
||||
studyprogramme: req.params.id,
|
||||
settings: {
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
})
|
||||
if (updatedStats) await setCreditStats(updatedStats, yearType, specialGroups)
|
||||
return res.json(updatedStats)
|
||||
})
|
||||
|
||||
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)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get code ${code} graduation stats`)
|
||||
}
|
||||
if (!data) {
|
||||
try {
|
||||
const result = await getGraduationStatsForStudytrack({
|
||||
studyprogramme: req.params.id,
|
||||
yearType,
|
||||
})
|
||||
data = await setGraduationStats(result, yearType)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} graduation stats`)
|
||||
}
|
||||
}
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
const data = await getGraduationStats(code, yearType, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
const updatedStats = await getGraduationStatsForStudytrack({
|
||||
studyprogramme: req.params.id,
|
||||
settings: {
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
})
|
||||
if (updatedStats) await setGraduationStats(updatedStats, yearType, specialGroups)
|
||||
return res.json(updatedStats)
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/coursestats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const showByYear = req.query.academicyear
|
||||
const date = new Date()
|
||||
date.setHours(23, 59, 59, 999)
|
||||
|
||||
try {
|
||||
const data = await getStudyprogrammeCoursesForStudytrack(date.getTime(), code, showByYear)
|
||||
return res.json(data)
|
||||
} else {
|
||||
res.status(422).end()
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get code ${code} programme courses stats: ${e}`)
|
||||
}
|
||||
})
|
||||
|
||||
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) return res.status(422).end()
|
||||
|
||||
const data = await getStudytrackStats(code, graduated, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
|
||||
const updated = await getStudytrackStatsForStudyprogramme({
|
||||
studyprogramme: code,
|
||||
settings: {
|
||||
graduated: graduated === 'GRADUATED_INCLUDED',
|
||||
specialGroups: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
})
|
||||
if (updated) await setStudytrackStats(updated, graduated, specialGroups)
|
||||
return res.json(updated)
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/update_basicview', async (req, res) => {
|
||||
const code = req.params.id
|
||||
if (code) {
|
||||
let data = null
|
||||
let result = null
|
||||
try {
|
||||
data = await getStudytrackStats(code)
|
||||
result = await updateBasicView(code)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get code ${code} studytrack stats: ${e}`)
|
||||
logger.error(`Failed to update code ${code} basic stats: ${e}`)
|
||||
}
|
||||
if (!data) {
|
||||
try {
|
||||
const result = await getStudytrackStatsForStudyprogramme({ studyprogramme: req.params.id })
|
||||
data = await setStudytrackStats(result)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} studytrack 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(data)
|
||||
return res.json(result)
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
|
||||
@@ -12,44 +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, user } = 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, user.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, user } = 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, user.id))
|
||||
res.status(200).json(filterRelevantTags(tags, id))
|
||||
})
|
||||
|
||||
router.delete('/tags', async (req, res) => {
|
||||
const { tag } = req.body
|
||||
const { rights, roles, user } = req
|
||||
const {
|
||||
user: { rights, roles, id },
|
||||
} = req
|
||||
|
||||
if (!rights.includes(tag.studytrack) && !(roles && roles.includes('admin')) && !(tag.personal_user_id === user.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, user.id))
|
||||
res.status(200).json(filterRelevantTags(t, id))
|
||||
})
|
||||
|
||||
router.get('/studenttags', async (req, res) => {
|
||||
@@ -59,12 +65,14 @@ router.get('/studenttags', async (req, res) => {
|
||||
|
||||
router.get('/studenttags/:studytrack', async (req, res) => {
|
||||
const { studytrack } = req.params
|
||||
const { rights, roles, user } = 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, user.id))
|
||||
res.status(200).json(filterRelevantStudentTags(result, id))
|
||||
})
|
||||
|
||||
router.get('/studenttags/:studentnumber', async (req, res) => {
|
||||
@@ -76,20 +84,24 @@ router.get('/studenttags/:studentnumber', async (req, res) => {
|
||||
|
||||
router.post('/studenttags/:studentnumber', async (req, res) => {
|
||||
const { tag, studytrack } = req.body
|
||||
const { rights, roles, user } = 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, user.id))
|
||||
res.status(200).json(filterRelevantStudentTags(result, id))
|
||||
})
|
||||
|
||||
router.post('/studenttags', async (req, res) => {
|
||||
const { tags, studytrack } = req.body
|
||||
const { rights, roles, user } = 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)
|
||||
@@ -106,35 +118,39 @@ router.post('/studenttags', async (req, res) => {
|
||||
|
||||
await TagStudent.createMultipleStudentTags(tags)
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantStudentTags(result, user.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, user } = 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, user.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, user } = 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, user.id))
|
||||
res.status(200).json(filterRelevantStudentTags(result, id))
|
||||
})
|
||||
|
||||
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) {
|
||||
|
||||
@@ -1,183 +1,104 @@
|
||||
const { redisClient } = require('../services/redis')
|
||||
const moment = require('moment')
|
||||
|
||||
const createRedisKeyForProductivity = id => `PRODUCTIVITY_${id}`
|
||||
const createRedisKeyForThroughput = id => `THROUGHPUT_${id}`
|
||||
const createRedisKeyForBasicStats = (id, yearType) => `BASIC_STATS_${id}_${yearType}`
|
||||
const createRedisKeyForCreditStats = (id, yearType) => `CREDIT_STATS_${id}_${yearType}`
|
||||
const createRedisKeyForGraduationStats = (id, yearType) => `GRADUATION_STATS_${id}_${yearType}`
|
||||
const createRedisKeyForStudytrackStats = id => `STUDYTRACK_STATS_${id}`
|
||||
// Only new bachelor, masters and doctoral programmes get their data updated in redis every night, use redis for them
|
||||
const isUpdatedNewProgramme = code => code.includes('KH') || code.includes('MH') || /^(T)[0-9]{6}$/.test(code)
|
||||
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 dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return {
|
||||
[id]: JSON.parse(dataFromRedis),
|
||||
}
|
||||
}
|
||||
|
||||
const setProductivity = async data => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForProductivity(id)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return {
|
||||
[id]: 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 dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return {
|
||||
[id]: JSON.parse(dataFromRedis),
|
||||
}
|
||||
}
|
||||
|
||||
const setThroughput = async data => {
|
||||
const { id, data: dataToSet } = data
|
||||
const redisKey = createRedisKeyForThroughput(id)
|
||||
const dataToRedis = {
|
||||
data: dataToSet.years,
|
||||
totals: dataToSet.totals,
|
||||
stTotals: dataToSet.stTotals,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return {
|
||||
[id]: 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,
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(patchedData))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return {
|
||||
[id]: patchedData,
|
||||
}
|
||||
}
|
||||
|
||||
const getBasicStats = async (id, yearType) => {
|
||||
const redisKey = createRedisKeyForBasicStats(id, yearType)
|
||||
const getBasicStats = async (id, yearType, specialGroups) => {
|
||||
if (!isUpdatedNewProgramme(id)) return null
|
||||
const redisKey = createRedisKeyForBasicStats(id, yearType, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setBasicStats = async (data, yearType) => {
|
||||
const setBasicStats = async (data, yearType, specialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForBasicStats(id, yearType)
|
||||
const redisKey = createRedisKeyForBasicStats(id, yearType, specialGroups)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
if (!isUpdatedNewProgramme(id)) return dataToRedis
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getCreditStats = async (id, yearType) => {
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType)
|
||||
const getCreditStats = async (id, yearType, specialGroups) => {
|
||||
if (!isUpdatedNewProgramme(id)) return null
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setCreditStats = async (data, yearType) => {
|
||||
const setCreditStats = async (data, yearType, specialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType)
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, specialGroups)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
if (!isUpdatedNewProgramme(id)) return dataToRedis
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getGraduationStats = async (id, yearType) => {
|
||||
const redisKey = createRedisKeyForGraduationStats(id, yearType)
|
||||
const getGraduationStats = async (id, yearType, specialGroups) => {
|
||||
if (!isUpdatedNewProgramme(id)) return null
|
||||
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) => {
|
||||
const setGraduationStats = async (data, yearType, specialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForGraduationStats(id, yearType)
|
||||
const redisKey = createRedisKeyForGraduationStats(id, yearType, specialGroups)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
if (!isUpdatedNewProgramme(id)) return dataToRedis
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getStudytrackStats = async id => {
|
||||
const redisKey = createRedisKeyForStudytrackStats(id)
|
||||
const getStudytrackStats = async (id, graduated, specialGroups) => {
|
||||
if (!isUpdatedNewProgramme(id)) return null
|
||||
const redisKey = createRedisKeyForStudytrackStats(id, graduated, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setStudytrackStats = async data => {
|
||||
const setStudytrackStats = async (data, graduated, specialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForStudytrackStats(id)
|
||||
const redisKey = createRedisKeyForStudytrackStats(id, graduated, specialGroups)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
if (!isUpdatedNewProgramme(id)) return dataToRedis
|
||||
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,
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
const LRU = require('lru-cache')
|
||||
|
||||
// Max 25 users can be stored in the cache
|
||||
const userDataCache = new LRU({
|
||||
max: 25,
|
||||
length: () => 1,
|
||||
maxAge: 1000 * 60 * 60,
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
userDataCache,
|
||||
}
|
||||
@@ -40,18 +40,22 @@ class CourseStatsCounter {
|
||||
triedOfPopulation: undefined,
|
||||
passingSemesters: this.initializePassingSemesters(),
|
||||
}
|
||||
this.enrollments = {
|
||||
enrolledNoGrade: new Set(),
|
||||
semesters: this.initializePassingSemesters({}),
|
||||
}
|
||||
this.grades = {}
|
||||
}
|
||||
|
||||
initializePassingSemesters() {
|
||||
initializePassingSemesters(initialValue = 0) {
|
||||
const passingSemesters = {
|
||||
BEFORE: 0,
|
||||
LATER: 0,
|
||||
BEFORE: initialValue,
|
||||
LATER: initialValue,
|
||||
}
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
passingSemesters[fall[i]] = 0
|
||||
passingSemesters[spring[i]] = 0
|
||||
passingSemesters[fall[i]] = initialValue
|
||||
passingSemesters[spring[i]] = initialValue
|
||||
}
|
||||
|
||||
return passingSemesters
|
||||
@@ -91,6 +95,13 @@ class CourseStatsCounter {
|
||||
}
|
||||
}
|
||||
|
||||
markEnrollment(studentnumber, state, semester) {
|
||||
if (!this.enrollments[state]) this.enrollments[state] = new Set()
|
||||
this.enrollments[state].add(studentnumber)
|
||||
if (!this.enrollments.semesters[semester][state]) this.enrollments.semesters[semester][state] = new Set()
|
||||
this.enrollments.semesters[semester][state].add(studentnumber)
|
||||
}
|
||||
|
||||
failedBefore(studentnumber) {
|
||||
return this.students.failed[studentnumber] !== undefined
|
||||
}
|
||||
@@ -173,6 +184,10 @@ class CourseStatsCounter {
|
||||
getFinalStats() {
|
||||
const stats = this.stats
|
||||
const students = this.students
|
||||
const allStudents = Object.keys(students.all)
|
||||
const totalEnrolledNoGrade = [...(this.enrollments.ENROLLED || [])].filter(
|
||||
student => !allStudents.includes(student)
|
||||
).length
|
||||
|
||||
stats.students = lengthOf(students.all)
|
||||
stats.passed = lengthOf(students.passed)
|
||||
@@ -185,6 +200,9 @@ class CourseStatsCounter {
|
||||
stats.triedOfPopulation = percentageOf(stats.students, this.studentsInTotal)
|
||||
stats.perStudent = stats.attempts / (stats.passed + stats.failed)
|
||||
stats.passingSemestersCumulative = this.getPassingSemestersCumulative()
|
||||
stats.totalStudents = stats.students + totalEnrolledNoGrade
|
||||
stats.totalEnrolledNoGrade = totalEnrolledNoGrade
|
||||
stats.percentageWithEnrollments = percentageOf(stats.passed, stats.totalStudents)
|
||||
|
||||
return {
|
||||
stats,
|
||||
|
||||
@@ -8,12 +8,23 @@ class CourseYearlyStatsCounter {
|
||||
}
|
||||
|
||||
initProgramme(code, name) {
|
||||
this.programmes[code] = { name, students: {}, passed: {}, credits: {} }
|
||||
this.programmes[code] = {
|
||||
name,
|
||||
students: {},
|
||||
passed: {},
|
||||
credits: {},
|
||||
}
|
||||
}
|
||||
|
||||
initFacultyYear(code) {
|
||||
const year = `${1949 + Number(code)}-${1950 + Number(code)}`
|
||||
this.facultyStats[code] = { year, allStudents: [], allPassed: [], faculties: {}, allCredits: 0 }
|
||||
this.facultyStats[code] = {
|
||||
year,
|
||||
allStudents: [],
|
||||
allPassed: [],
|
||||
faculties: {},
|
||||
allCredits: 0,
|
||||
}
|
||||
}
|
||||
|
||||
initFaculty(yearcode, faculty_code, organization) {
|
||||
@@ -43,8 +54,11 @@ class CourseYearlyStatsCounter {
|
||||
passedEventually: [],
|
||||
neverPassed: [],
|
||||
},
|
||||
grades: {},
|
||||
studentnumbers: [],
|
||||
},
|
||||
enrollments: [],
|
||||
allEnrollments: [],
|
||||
yearcode,
|
||||
}
|
||||
}
|
||||
@@ -97,11 +111,41 @@ class CourseYearlyStatsCounter {
|
||||
})
|
||||
}
|
||||
|
||||
markStudyProgrammesEnrollment(studentnumber, programmes, yearcode, state, enrollment_date_time) {
|
||||
programmes.forEach(({ code, name, faculty_code, organization }) => {
|
||||
this.markStudyProgrammeEnrollment(
|
||||
code,
|
||||
name,
|
||||
studentnumber,
|
||||
yearcode,
|
||||
state,
|
||||
enrollment_date_time,
|
||||
faculty_code,
|
||||
organization
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
markCreditToGroup(studentnumber, passed, grade, groupcode, groupname, coursecode, yearcode) {
|
||||
if (!this.groups[groupcode]) {
|
||||
this.initGroup(groupcode, groupname, coursecode, yearcode)
|
||||
}
|
||||
this.markCreditToAttempts(studentnumber, passed, grade, groupcode)
|
||||
this.markBestEffortGrade(studentnumber, passed, grade, groupcode)
|
||||
}
|
||||
|
||||
markEnrollmentToGroup(studentnumber, state, enrollment_date_time, groupcode, groupname, coursecode, yearcode) {
|
||||
if (!this.groups[groupcode]) this.initGroup(groupcode, groupname, coursecode, yearcode)
|
||||
|
||||
const enrollment = { studentnumber, state, enrollment_date_time }
|
||||
this.groups[groupcode].allEnrollments.push(enrollment)
|
||||
const oldEnrollment = this.groups[groupcode].enrollments.find(e => e.studentnumber === studentnumber)
|
||||
if (!oldEnrollment) return this.groups[groupcode].enrollments.push({ studentnumber, state, enrollment_date_time })
|
||||
if (oldEnrollment.state === 'ENROLLED' || oldEnrollment.state === 'CONFIRMED') return
|
||||
if (state !== 'ENROLLED' || state !== 'CONFIRMED') return
|
||||
this.groups[groupcode].enrollments = this.groups[groupcode].enrollments
|
||||
.filter(e => e.studentnumber !== studentnumber)
|
||||
.concat([enrollment])
|
||||
}
|
||||
|
||||
markCreditToAttempts(studentnumber, passed, grade, groupcode) {
|
||||
@@ -118,6 +162,19 @@ class CourseYearlyStatsCounter {
|
||||
}
|
||||
}
|
||||
|
||||
markBestEffortGrade(studentnumber, passed, grade, groupcode) {
|
||||
const current = this.groups[groupcode].students.grades[studentnumber]
|
||||
|
||||
if (!current) {
|
||||
this.groups[groupcode].students.grades[studentnumber] = { grade, passed }
|
||||
return
|
||||
}
|
||||
|
||||
if (current.passed && !passed) return
|
||||
if (current.passed && Number(current.grade) >= Number(grade)) return
|
||||
this.groups[groupcode].students.grades[studentnumber] = { grade, passed }
|
||||
}
|
||||
|
||||
markCreditToStudentCategories(studentnumber, passed, attainment_date, groupcode) {
|
||||
if (!this.students.has(studentnumber)) {
|
||||
if (passed) {
|
||||
@@ -183,8 +240,17 @@ class CourseYearlyStatsCounter {
|
||||
}
|
||||
|
||||
const groupStatistics = Object.values(this.groups).map(({ ...rest }) => {
|
||||
const { students } = rest
|
||||
const grades = {}
|
||||
Object.keys(students.grades).forEach(student => {
|
||||
const { grade, passed } = students.grades[student]
|
||||
const parsed = passed ? grade : '0'
|
||||
if (!grades[parsed]) grades[parsed] = []
|
||||
grades[parsed].push(student)
|
||||
})
|
||||
const normalStats = {
|
||||
...rest,
|
||||
students: { ...students, grades },
|
||||
}
|
||||
|
||||
if (anonymizationSalt && normalStats.students.studentnumbers.length < 6) {
|
||||
|
||||
@@ -8,12 +8,14 @@ const {
|
||||
Studyright,
|
||||
Semester,
|
||||
Organization,
|
||||
Enrollment,
|
||||
} = require('../models')
|
||||
|
||||
const { parseCredit } = require('./parseCredits')
|
||||
const Op = Sequelize.Op
|
||||
const { CourseYearlyStatsCounter } = require('./course_yearly_stats_counter')
|
||||
const { sortMainCode, getSortRank } = require('../util/utils')
|
||||
const { parseEnrollment } = require('./parseEnrollments')
|
||||
|
||||
const byNameOrCode = (searchTerm, language) =>
|
||||
Course.findAll({
|
||||
@@ -59,10 +61,12 @@ const findOneByCode = code => {
|
||||
})
|
||||
}
|
||||
|
||||
const creditsForCourses = async (codes, anonymizationSalt, unifyOpenUniCourses) => {
|
||||
const creditsForCourses = async (codes, anonymizationSalt, unification) => {
|
||||
let is_open = false
|
||||
|
||||
if (unifyOpenUniCourses) {
|
||||
if (unification === 'open') is_open = true
|
||||
|
||||
if (unification === 'unify') {
|
||||
is_open = {
|
||||
[Op.in]: [false, true],
|
||||
}
|
||||
@@ -104,6 +108,11 @@ const creditsForCourses = async (codes, anonymizationSalt, unifyOpenUniCourses)
|
||||
{
|
||||
model: Semester,
|
||||
attributes: ['semestercode', 'name', 'yearcode', 'yearname'],
|
||||
where: {
|
||||
startdate: {
|
||||
[Op.lte]: new Date(),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
@@ -122,6 +131,75 @@ const creditsForCourses = async (codes, anonymizationSalt, unifyOpenUniCourses)
|
||||
return parsedCredits
|
||||
}
|
||||
|
||||
const enrollmentsForCourses = async (codes, anonymizationSalt, unification) => {
|
||||
let is_open = false
|
||||
|
||||
if (unification === 'open') is_open = true
|
||||
|
||||
if (unification === 'unify') {
|
||||
is_open = {
|
||||
[Op.in]: [false, true],
|
||||
}
|
||||
}
|
||||
|
||||
const enrollments = await Enrollment.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
include: {
|
||||
model: StudyrightElement,
|
||||
attributes: ['code', 'startdate'],
|
||||
include: [
|
||||
{
|
||||
model: ElementDetail,
|
||||
attributes: ['name', 'type'],
|
||||
where: {
|
||||
type: {
|
||||
[Op.eq]: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
model: Studyright,
|
||||
attributes: ['prioritycode', 'faculty_code'],
|
||||
where: {
|
||||
prioritycode: {
|
||||
[Op.eq]: 1,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
model: Organization,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
model: Semester,
|
||||
attributes: ['semestercode', 'name', 'yearcode', 'yearname'],
|
||||
where: {
|
||||
startdate: {
|
||||
[Op.lte]: new Date(),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
course_code: {
|
||||
[Op.in]: codes,
|
||||
},
|
||||
studentnumber: {
|
||||
[Op.ne]: null,
|
||||
},
|
||||
enrollment_date_time: { [Op.gte]: new Date('2021-05-31') },
|
||||
state: ['ENROLLED', 'CONFIRMED'],
|
||||
[Op.or]: [{ is_open }, { is_open: null }],
|
||||
},
|
||||
})
|
||||
return enrollments.map(enrollment => parseEnrollment(enrollment, anonymizationSalt))
|
||||
}
|
||||
|
||||
const bySearchTerm = async (term, language) => {
|
||||
const formatCourse = course => ({
|
||||
name: course.name[language],
|
||||
@@ -162,23 +240,20 @@ const allCodeAlternatives = async code => {
|
||||
return sortMainCode([...course.substitutions, code])
|
||||
}
|
||||
|
||||
const yearlyStatsOfNew = async (coursecode, separate, unifyOpenUniCourses, anonymizationSalt) => {
|
||||
const yearlyStatsOfNew = async (coursecode, separate, unification, anonymizationSalt) => {
|
||||
const courseForSubs = await Course.findOne({
|
||||
where: { code: coursecode },
|
||||
})
|
||||
|
||||
let codes = courseForSubs.substitutions ? sortMainCode([...courseForSubs.substitutions, coursecode]) : [coursecode]
|
||||
|
||||
if (isOpenUniCourseCode(coursecode) && !unifyOpenUniCourses) {
|
||||
codes = [coursecode]
|
||||
if (unification === 'reqular') {
|
||||
codes = codes.filter(course => !isOpenUniCourseCode(course))
|
||||
}
|
||||
|
||||
if (!isOpenUniCourseCode(coursecode) && !unifyOpenUniCourses) {
|
||||
codes = codes.filter(code => !code.match(/^[A][0-9]|^AY/))
|
||||
}
|
||||
|
||||
const [credits, course] = await Promise.all([
|
||||
creditsForCourses(codes, anonymizationSalt, unifyOpenUniCourses),
|
||||
const [credits, enrollments, course] = await Promise.all([
|
||||
creditsForCourses(codes, anonymizationSalt, unification),
|
||||
enrollmentsForCourses(codes, anonymizationSalt, unification),
|
||||
Course.findOne({
|
||||
where: {
|
||||
code: coursecode,
|
||||
@@ -233,7 +308,27 @@ const yearlyStatsOfNew = async (coursecode, separate, unifyOpenUniCourses, anony
|
||||
counter.markCreditToGroup(studentnumber, passed, grade, groupcode, groupname, coursecode, yearcode)
|
||||
counter.markCreditToStudentCategories(studentnumber, passed, attainment_date, groupcode)
|
||||
}
|
||||
|
||||
enrollments.forEach(enrollment => {
|
||||
const { studentnumber, semestercode, semestername, yearcode, yearname, coursecode, state, enrollment_date_time } =
|
||||
enrollment
|
||||
|
||||
const groupcode = separate ? semestercode : yearcode
|
||||
const groupname = separate ? semestername : yearname
|
||||
|
||||
counter.markEnrollmentToGroup(
|
||||
studentnumber,
|
||||
state,
|
||||
enrollment_date_time,
|
||||
groupcode,
|
||||
groupname,
|
||||
coursecode,
|
||||
yearcode
|
||||
)
|
||||
})
|
||||
|
||||
const statistics = counter.getFinalStatistics(anonymizationSalt)
|
||||
|
||||
return {
|
||||
...statistics,
|
||||
coursecode,
|
||||
@@ -242,7 +337,17 @@ const yearlyStatsOfNew = async (coursecode, separate, unifyOpenUniCourses, anony
|
||||
}
|
||||
}
|
||||
|
||||
const maxYearsToCreatePopulationFrom = async coursecodes => {
|
||||
const maxYearsToCreatePopulationFrom = async (coursecodes, unifyCourses) => {
|
||||
let is_open = false
|
||||
|
||||
if (unifyCourses === 'openStats') is_open = true
|
||||
|
||||
if (unifyCourses === 'unifyStats') {
|
||||
is_open = {
|
||||
[Op.in]: [false, true],
|
||||
}
|
||||
}
|
||||
|
||||
const maxAttainmentDate = new Date(
|
||||
Math.max(
|
||||
...(
|
||||
@@ -257,6 +362,7 @@ const maxYearsToCreatePopulationFrom = async coursecodes => {
|
||||
).map(c => new Date(c.max_attainment_date).getTime())
|
||||
)
|
||||
)
|
||||
|
||||
const attainmentThreshold = new Date(maxAttainmentDate.getFullYear(), 0, 1)
|
||||
attainmentThreshold.setFullYear(attainmentThreshold.getFullYear() - 6)
|
||||
|
||||
@@ -268,6 +374,7 @@ const maxYearsToCreatePopulationFrom = async coursecodes => {
|
||||
attainment_date: {
|
||||
[Op.gt]: attainmentThreshold,
|
||||
},
|
||||
is_open,
|
||||
},
|
||||
order: [['attainment_date', 'ASC']],
|
||||
})
|
||||
@@ -291,11 +398,18 @@ const maxYearsToCreatePopulationFrom = async coursecodes => {
|
||||
return maxYearsToCreatePopulationFrom
|
||||
}
|
||||
|
||||
const courseYearlyStats = async (coursecodes, separate, unifyOpenUniCourses, anonymizationSalt) => {
|
||||
const stats = await Promise.all(
|
||||
coursecodes.map(code => yearlyStatsOfNew(code, separate, unifyOpenUniCourses, anonymizationSalt))
|
||||
const courseYearlyStats = async (coursecodes, separate, anonymizationSalt) => {
|
||||
const statsReqular = await Promise.all(
|
||||
coursecodes.map(async code => {
|
||||
const unifyStats = await yearlyStatsOfNew(code, separate, 'unify', anonymizationSalt)
|
||||
const reqularStats = await yearlyStatsOfNew(code, separate, 'reqular', anonymizationSalt)
|
||||
const openStats = await yearlyStatsOfNew(code, separate, 'open', anonymizationSalt)
|
||||
|
||||
return { unifyStats, reqularStats, openStats }
|
||||
})
|
||||
)
|
||||
return stats
|
||||
|
||||
return statsReqular
|
||||
}
|
||||
|
||||
const nameLikeTerm = name => {
|
||||
@@ -335,7 +449,8 @@ const codeLikeTerm = code =>
|
||||
? undefined
|
||||
: {
|
||||
code: {
|
||||
[Op.iLike]: `%${code.trim()}%`,
|
||||
// Starts with code or has AY/A in front of the code
|
||||
[Op.iRegexp]: `^(AY|ay|A|a)?${code}`,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -381,6 +496,23 @@ const byNameAndOrCodeLike = async (name, code) => {
|
||||
return { courses }
|
||||
}
|
||||
|
||||
const getCourseProvidersForCourses = async codes => {
|
||||
return (
|
||||
await Organization.findAll({
|
||||
attributes: ['code'],
|
||||
include: {
|
||||
model: Course,
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: codes,
|
||||
},
|
||||
},
|
||||
},
|
||||
raw: true,
|
||||
})
|
||||
).map(({ code }) => code)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
byCode,
|
||||
byName,
|
||||
@@ -393,4 +525,5 @@ module.exports = {
|
||||
unifyOpenUniversity,
|
||||
allCodeAlternatives,
|
||||
findOneByCode,
|
||||
getCourseProvidersForCourses,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
const { randomBytes, createCipheriv, createDecipheriv } = require('crypto')
|
||||
|
||||
const algorithm = 'aes-256-cbc'
|
||||
const key = randomBytes(32)
|
||||
const iv = randomBytes(16)
|
||||
|
||||
const encrypt = text => {
|
||||
const cipher = createCipheriv(algorithm, Buffer.from(key), iv)
|
||||
let encrypted = cipher.update(text)
|
||||
encrypted = Buffer.concat([encrypted, cipher.final()])
|
||||
|
||||
return {
|
||||
iv: iv.toString('hex'),
|
||||
encryptedData: encrypted.toString('hex'),
|
||||
}
|
||||
}
|
||||
|
||||
const decrypt = text => {
|
||||
const iv = Buffer.from(text.iv, 'hex')
|
||||
const encryptedText = Buffer.from(text.encryptedData, 'hex')
|
||||
const decipher = createDecipheriv(algorithm, Buffer.from(key), iv)
|
||||
let decrypted = decipher.update(encryptedText)
|
||||
decrypted = Buffer.concat([decrypted, decipher.final()])
|
||||
|
||||
return decrypted.toString()
|
||||
}
|
||||
|
||||
module.exports = { encrypt, decrypt }
|
||||
@@ -0,0 +1,351 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { Op } = Sequelize
|
||||
const {
|
||||
ElementDetail,
|
||||
Organization,
|
||||
ProgrammeModule,
|
||||
Semester,
|
||||
SemesterEnrollment,
|
||||
Studyright,
|
||||
StudyrightElement,
|
||||
Student,
|
||||
Transfer,
|
||||
Credit,
|
||||
Course,
|
||||
} = require('../../models')
|
||||
const {
|
||||
facultyFormatStudyright,
|
||||
facultyFormatProgramme,
|
||||
formatFacultyTransfer,
|
||||
formatFacultyThesisWriter,
|
||||
formatOrganization,
|
||||
formatAbsence,
|
||||
isNewProgramme,
|
||||
mapCodesToIds,
|
||||
} = require('./facultyHelpers')
|
||||
|
||||
const getTransferredToAndAway = async (programmeCodes, allProgrammeCodes, since) => {
|
||||
const awayTransfers = (await transferredAway(programmeCodes, allProgrammeCodes, since)).map(t => t.studyrightid)
|
||||
const toTransfers = (await transferredTo(programmeCodes, allProgrammeCodes, since)).map(t => t.studyrightid)
|
||||
const transferredToOrAway = [...toTransfers, ...awayTransfers]
|
||||
return transferredToOrAway
|
||||
}
|
||||
|
||||
const getTransferredInside = async (programmeCodes, allProgrammeCodes, since) => {
|
||||
const transfersInside = (await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)).map(
|
||||
t => t.studyrightid
|
||||
)
|
||||
return transfersInside
|
||||
}
|
||||
const startedStudyrights = async (faculty, since, studyRightWhere) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
faculty_code: faculty,
|
||||
studystartdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
student_studentnumber: { [Op.not]: null },
|
||||
...studyRightWhere,
|
||||
},
|
||||
})
|
||||
).map(facultyFormatStudyright)
|
||||
|
||||
const graduatedStudyrights = async (faculty, since, studyrightWhere) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
faculty_code: faculty,
|
||||
enddate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
graduated: 1,
|
||||
student_studentnumber: { [Op.not]: null },
|
||||
...studyrightWhere,
|
||||
},
|
||||
})
|
||||
).map(facultyFormatStudyright)
|
||||
|
||||
const studyrightsByRightStartYear = async (faculty, since, graduated = 1) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
faculty_code: faculty,
|
||||
startdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
graduated: graduated,
|
||||
student_studentnumber: { [Op.not]: null },
|
||||
},
|
||||
})
|
||||
).map(facultyFormatStudyright)
|
||||
|
||||
const bachelorStudyright = async id => {
|
||||
return await Studyright.findOne({
|
||||
attributes: ['studystartdate'],
|
||||
where: {
|
||||
studyrightid: id,
|
||||
extentcode: 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const hasMasterRight = async id => {
|
||||
return await Studyright.findOne({
|
||||
where: {
|
||||
studyrightid: id,
|
||||
extentcode: 2,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const transferredInsideFaculty = async (programmes, allProgrammeCodes, since) =>
|
||||
(
|
||||
await Transfer.findAll({
|
||||
where: {
|
||||
transferdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
sourcecode: allProgrammeCodes,
|
||||
targetcode: programmes,
|
||||
},
|
||||
})
|
||||
).map(formatFacultyTransfer)
|
||||
|
||||
const transferredAway = async (programmes, allProgrammeCodes, since) =>
|
||||
(
|
||||
await Transfer.findAll({
|
||||
where: {
|
||||
transferdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
sourcecode: programmes,
|
||||
targetcode: {
|
||||
[Op.notIn]: allProgrammeCodes,
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(formatFacultyTransfer)
|
||||
|
||||
const transferredTo = async (programmes, allProgrammeCodes, since) =>
|
||||
(
|
||||
await Transfer.findAll({
|
||||
where: {
|
||||
transferdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
sourcecode: {
|
||||
[Op.notIn]: allProgrammeCodes,
|
||||
},
|
||||
targetcode: programmes,
|
||||
},
|
||||
})
|
||||
).map(formatFacultyTransfer)
|
||||
|
||||
const degreeProgrammesOfFaculty = async facultyCode =>
|
||||
(
|
||||
await ProgrammeModule.findAll({
|
||||
attributes: ['code', 'name'],
|
||||
include: {
|
||||
model: Organization,
|
||||
where: {
|
||||
code: {
|
||||
[Op.startsWith]: facultyCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(facultyFormatProgramme)
|
||||
|
||||
const facultyOgranizationId = async faculty => {
|
||||
return await Organization.findOne({
|
||||
attributes: ['id'],
|
||||
where: {
|
||||
code: faculty,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const getChildOrganizations = async facultyId =>
|
||||
(
|
||||
await Organization.findAll({
|
||||
attributes: ['id', 'name', 'code', 'parent_id'],
|
||||
where: {
|
||||
parent_id: facultyId,
|
||||
},
|
||||
})
|
||||
).map(formatOrganization)
|
||||
|
||||
const thesisWriters = async (provider, since, thesisTypes, students) =>
|
||||
(
|
||||
await Credit.findAll({
|
||||
attributes: ['id', 'course_code', 'attainment_date', 'student_studentnumber'],
|
||||
include: {
|
||||
model: Course,
|
||||
attributes: ['course_unit_type'],
|
||||
required: true,
|
||||
where: {
|
||||
course_unit_type: {
|
||||
[Op.in]: thesisTypes,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
model: Organization,
|
||||
attributes: [],
|
||||
required: true,
|
||||
where: {
|
||||
code: provider,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
credittypecode: 4,
|
||||
isStudyModule: {
|
||||
[Op.not]: true,
|
||||
},
|
||||
attainment_date: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
student_studentnumber: students.length > 0 ? students : { [Op.not]: null },
|
||||
},
|
||||
})
|
||||
).map(formatFacultyThesisWriter)
|
||||
|
||||
const statutoryAbsences = async (studentnumber, startdate, enddate) =>
|
||||
(
|
||||
await SemesterEnrollment.findAll({
|
||||
attributes: ['semestercode'],
|
||||
include: {
|
||||
model: Semester,
|
||||
attributes: ['startdate', 'enddate'],
|
||||
where: {
|
||||
startdate: {
|
||||
[Op.gte]: startdate,
|
||||
},
|
||||
enddate: {
|
||||
[Op.lte]: enddate,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
studentnumber: studentnumber,
|
||||
statutory_absence: true,
|
||||
},
|
||||
})
|
||||
).map(formatAbsence)
|
||||
|
||||
// Some programme modules are not directly associated to a faculty (organization).
|
||||
// Some have intermediate organizations, such as department, so the connection must be digged up
|
||||
const findFacultyProgrammeCodes = async (faculty, programmeFilter) => {
|
||||
let allProgrammes = []
|
||||
let allProgrammeCodes = []
|
||||
|
||||
const directAssociationProgrammes = await degreeProgrammesOfFaculty(faculty)
|
||||
|
||||
allProgrammes = allProgrammes.concat(directAssociationProgrammes)
|
||||
allProgrammeCodes = allProgrammeCodes.concat(directAssociationProgrammes.map(prog => prog.code))
|
||||
|
||||
// find faculty's organization id and its direct child organizations
|
||||
const { id } = await facultyOgranizationId(faculty)
|
||||
const facultyChildOrganizations = await getChildOrganizations(id)
|
||||
|
||||
// get programme modules that have a faculty child as organization(_id)
|
||||
for (const org of facultyChildOrganizations) {
|
||||
const childAssociationProgrammes = await degreeProgrammesOfFaculty(org.code)
|
||||
if (childAssociationProgrammes.length > 0) {
|
||||
childAssociationProgrammes.forEach(prog => {
|
||||
if (!(prog.code in allProgrammeCodes)) {
|
||||
allProgrammes = allProgrammes.concat([prog])
|
||||
allProgrammeCodes = allProgrammeCodes.concat([prog.code])
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// dig deeper
|
||||
const grandChildren = await getChildOrganizations(org.id)
|
||||
if (grandChildren.length > 0) {
|
||||
for (const gcOrg of grandChildren) {
|
||||
const associatedProgrammes = await degreeProgrammesOfFaculty(gcOrg.code)
|
||||
if (associatedProgrammes.length > 0) {
|
||||
associatedProgrammes.forEach(prog => {
|
||||
if (!(prog.code in allProgrammeCodes)) {
|
||||
allProgrammes = allProgrammes.concat([prog])
|
||||
allProgrammeCodes = allProgrammeCodes.concat([prog.code])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (programmeFilter === 'NEW_STUDY_PROGRAMMES') {
|
||||
allProgrammes = allProgrammes.filter(prog => isNewProgramme(prog.code))
|
||||
}
|
||||
|
||||
mapCodesToIds(allProgrammes)
|
||||
|
||||
return allProgrammes
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
startedStudyrights,
|
||||
graduatedStudyrights,
|
||||
studyrightsByRightStartYear,
|
||||
bachelorStudyright,
|
||||
hasMasterRight,
|
||||
transferredInsideFaculty,
|
||||
transferredAway,
|
||||
transferredTo,
|
||||
degreeProgrammesOfFaculty,
|
||||
thesisWriters,
|
||||
findFacultyProgrammeCodes,
|
||||
facultyOgranizationId,
|
||||
statutoryAbsences,
|
||||
getTransferredToAndAway,
|
||||
getTransferredInside,
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
const { indexOf } = require('lodash')
|
||||
const {
|
||||
startedStudyrights,
|
||||
graduatedStudyrights,
|
||||
transferredInsideFaculty,
|
||||
transferredAway,
|
||||
transferredTo,
|
||||
getTransferredToAndAway,
|
||||
getTransferredInside,
|
||||
} = require('./faculty')
|
||||
const { getStatsBasis, getYearsArray, defineYear } = require('../studyprogrammeHelpers')
|
||||
const { findRightProgramme, isNewProgramme, checkTransfers, getExtentFilter } = require('./facultyHelpers')
|
||||
const { codes } = require('../../../config/programmeCodes')
|
||||
|
||||
const filterDuplicateStudyrights = studyrights => {
|
||||
// bachelor+master students have two studyrights (separated by two last digits in studyrightid)
|
||||
// choose only the earlier started one, so we don't count start of masters as starting in faculty
|
||||
let rightsToCount = {}
|
||||
|
||||
studyrights.forEach(right => {
|
||||
const id = right.studyrightid.slice(0, -2)
|
||||
const start = new Date(right.studystartdate)
|
||||
if (id in rightsToCount) {
|
||||
if (new Date(rightsToCount[id].studystartdate) > start) {
|
||||
rightsToCount[id] = right
|
||||
}
|
||||
} else {
|
||||
rightsToCount[id] = right
|
||||
}
|
||||
})
|
||||
return Object.values(rightsToCount)
|
||||
}
|
||||
|
||||
const addProgramme = async (
|
||||
stats,
|
||||
programme,
|
||||
tableStats,
|
||||
transferYear,
|
||||
index,
|
||||
allBasics,
|
||||
facultyProgrammes,
|
||||
includeAllSpecials
|
||||
) => {
|
||||
let { progId, name, code } = facultyProgrammes.filter(prog => prog.code === programme)[0]
|
||||
if (!(progId in stats)) {
|
||||
stats[progId] = {}
|
||||
if (includeAllSpecials) {
|
||||
Object.keys(tableStats).forEach(year => (stats[progId][year] = [0, 0, 0]))
|
||||
} else {
|
||||
Object.keys(tableStats).forEach(year => (stats[progId][year] = [0]))
|
||||
}
|
||||
}
|
||||
stats[progId][transferYear][index] += 1
|
||||
if (!(progId in allBasics.programmeNames)) {
|
||||
allBasics.programmeNames[progId] = { ...name, code: code }
|
||||
}
|
||||
}
|
||||
|
||||
const getFacultyStarters = async (
|
||||
faculty,
|
||||
transfersToOrAwayStudyrights,
|
||||
insideTransfersStudyrights,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
counts,
|
||||
allBasics,
|
||||
programmeData,
|
||||
programmeFilter,
|
||||
includeAllSpecials
|
||||
) => {
|
||||
const startedGraphStats = [...graphStats]
|
||||
const startedTableStats = { ...tableStats }
|
||||
const studyrightWhere = getExtentFilter(includeAllSpecials)
|
||||
const studyrights = await startedStudyrights(faculty, since, studyrightWhere)
|
||||
let filteredStudyrights = filterDuplicateStudyrights(studyrights)
|
||||
const keys = Object.keys(codes)
|
||||
const programmeTableStats = {}
|
||||
if (!includeAllSpecials) {
|
||||
filteredStudyrights = filteredStudyrights.filter(
|
||||
s => !checkTransfers(s, insideTransfersStudyrights, transfersToOrAwayStudyrights)
|
||||
)
|
||||
}
|
||||
|
||||
filteredStudyrights.forEach(({ studystartdate, studyrightElements }) => {
|
||||
const { programme, programmeName } = findRightProgramme(studyrightElements, 'started')
|
||||
let programmeId = programme
|
||||
|
||||
if (keys.includes(programme)) {
|
||||
programmeId = codes[programme].toUpperCase()
|
||||
}
|
||||
|
||||
if (programmeFilter === 'ALL_PROGRAMMES' || isNewProgramme(programme)) {
|
||||
const startYear = defineYear(studystartdate, isAcademicYear)
|
||||
|
||||
startedGraphStats[indexOf(yearsArray, startYear)] += 1
|
||||
startedTableStats[startYear] += 1
|
||||
|
||||
if (!(programmeId in programmeTableStats)) {
|
||||
programmeTableStats[programmeId] = { ...tableStats }
|
||||
}
|
||||
programmeTableStats[programmeId][startYear] += 1
|
||||
|
||||
if (!(programmeId in allBasics.programmeNames)) {
|
||||
allBasics.programmeNames[programmeId] = { ...programmeName, code: programme }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Started studying', data: startedGraphStats })
|
||||
programmeData['started'] = programmeTableStats
|
||||
|
||||
Object.keys(startedTableStats).forEach(year => {
|
||||
counts[year] = [startedTableStats[year]]
|
||||
})
|
||||
}
|
||||
|
||||
const getFacultyGraduates = async (
|
||||
faculty,
|
||||
transfersToOrAwayStudyrights,
|
||||
insideTransfersStudyrights,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
allBasics,
|
||||
counts,
|
||||
countsGraduations,
|
||||
programmeData,
|
||||
programmeFilter,
|
||||
includeAllSpecials
|
||||
) => {
|
||||
const graduatedGraphStats = [[...graphStats], [...graphStats], [...graphStats], [...graphStats], [...graphStats]]
|
||||
const graduatedTableStats = {}
|
||||
const keys = Object.keys(codes)
|
||||
let studyrightWhere = getExtentFilter(includeAllSpecials)
|
||||
let graduatedRights = await graduatedStudyrights(faculty, since, studyrightWhere)
|
||||
const programmeTableStats = {}
|
||||
|
||||
Object.keys(tableStats).forEach(year => (graduatedTableStats[year] = [0, 0, 0, 0, 0]))
|
||||
if (!includeAllSpecials) {
|
||||
graduatedRights = graduatedRights.filter(
|
||||
s => !checkTransfers(s, insideTransfersStudyrights, transfersToOrAwayStudyrights)
|
||||
)
|
||||
}
|
||||
graduatedRights.forEach(({ enddate, extentcode, studyrightElements }) => {
|
||||
const { programme, programmeName } = findRightProgramme(studyrightElements, 'graduated')
|
||||
let programmeId = programme
|
||||
|
||||
if (keys.includes(programme)) {
|
||||
programmeId = codes[programme].toUpperCase()
|
||||
}
|
||||
if (programmeFilter === 'ALL_PROGRAMMES' || isNewProgramme(programme)) {
|
||||
const endYear = defineYear(enddate, isAcademicYear)
|
||||
graduatedGraphStats[0][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][0] += 1
|
||||
|
||||
if (!(programmeId in programmeTableStats)) {
|
||||
programmeTableStats[programmeId] = {}
|
||||
Object.keys(tableStats).forEach(year => (programmeTableStats[programmeId][year] = [0, 0, 0, 0, 0]))
|
||||
}
|
||||
programmeTableStats[programmeId][endYear][0] += 1
|
||||
|
||||
if (extentcode === 1) {
|
||||
graduatedGraphStats[1][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][1] += 1
|
||||
programmeTableStats[programmeId][endYear][1] += 1
|
||||
} else if (extentcode === 2) {
|
||||
graduatedGraphStats[2][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][2] += 1
|
||||
programmeTableStats[programmeId][endYear][2] += 1
|
||||
} else if (extentcode === 4) {
|
||||
graduatedGraphStats[3][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][3] += 1
|
||||
programmeTableStats[programmeId][endYear][3] += 1
|
||||
} else {
|
||||
graduatedGraphStats[4][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][4] += 1
|
||||
programmeTableStats[programmeId][endYear][4] += 1
|
||||
}
|
||||
|
||||
if (!(programmeId in allBasics.programmeNames)) {
|
||||
allBasics.programmeNames[programmeId] = { code: programme, ...programmeName }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Graduated', data: graduatedGraphStats[0] })
|
||||
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'Bachelors', data: graduatedGraphStats[1] })
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'Masters', data: graduatedGraphStats[2] })
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'Doctors', data: graduatedGraphStats[3] })
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'Others', data: graduatedGraphStats[4] })
|
||||
|
||||
programmeData['graduated'] = programmeTableStats
|
||||
|
||||
Object.keys(graduatedTableStats).forEach(year => {
|
||||
counts[year] = counts[year].concat(graduatedTableStats[year][0])
|
||||
countsGraduations[year] = graduatedTableStats[year]
|
||||
})
|
||||
}
|
||||
|
||||
const getFacultyTransfers = async (
|
||||
programmes,
|
||||
programmeCodes,
|
||||
allProgrammeCodes,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
allBasics,
|
||||
counts,
|
||||
programmeData,
|
||||
includeAllSpecials
|
||||
) => {
|
||||
const insideTransfers = await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
|
||||
// Filter still away those that have started studying before 1.8.2017, when include all specials false.
|
||||
let transferGraphStats = []
|
||||
let transferTableStats = {}
|
||||
if (includeAllSpecials) {
|
||||
transferGraphStats = [[...graphStats], [...graphStats], [...graphStats]]
|
||||
Object.keys(tableStats).forEach(year => (transferTableStats[year] = [0, 0, 0]))
|
||||
} else {
|
||||
transferGraphStats = [[...graphStats]]
|
||||
Object.keys(tableStats).forEach(year => (transferTableStats[year] = [0]))
|
||||
}
|
||||
|
||||
const programmeTableStats = {}
|
||||
// inside transfer is counted towards target
|
||||
for (const { transferdate, targetcode } of insideTransfers) {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[0][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][0] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
targetcode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
0,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
}
|
||||
if (includeAllSpecials) {
|
||||
const awayTransfers = await transferredAway(programmeCodes, allProgrammeCodes, since)
|
||||
const toTransfers = await transferredTo(programmeCodes, allProgrammeCodes, since)
|
||||
|
||||
for (const { transferdate, sourcecode } of awayTransfers) {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[1][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][1] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
sourcecode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
1,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
}
|
||||
|
||||
for (const { transferdate, targetcode } of toTransfers) {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[2][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][2] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
targetcode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
2,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Transferred inside', data: transferGraphStats[0] })
|
||||
if (includeAllSpecials) {
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Transferred away', data: transferGraphStats[1] })
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Transferred to', data: transferGraphStats[2] })
|
||||
}
|
||||
programmeData['transferred'] = programmeTableStats
|
||||
|
||||
Object.keys(transferTableStats).forEach(year => {
|
||||
counts[year] = counts[year].concat(transferTableStats[year])
|
||||
})
|
||||
}
|
||||
|
||||
const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeCodes, programmeFilter, special) => {
|
||||
let counts = {}
|
||||
let countsGraduations = {}
|
||||
let years = []
|
||||
let programmeData = {}
|
||||
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
|
||||
const includeAllSpecials = special === 'SPECIAL_INCLUDED'
|
||||
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
|
||||
const yearsArray = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
const { graphStats, tableStats } = getStatsBasis(yearsArray)
|
||||
Object.keys(tableStats).forEach(year => years.push(year))
|
||||
const wantedProgrammeCodes = programmes.map(prog => prog.code)
|
||||
|
||||
let allBasics = {
|
||||
id: faculty,
|
||||
years: [],
|
||||
programmeNames: {},
|
||||
studentInfo: {
|
||||
tableStats: [],
|
||||
graphStats: [],
|
||||
titles: includeAllSpecials
|
||||
? ['', 'Started studying', 'Graduated', 'Transferred inside', 'Transferred away', 'Transferred to']
|
||||
: ['', 'Started studying', 'Graduated', 'Transferred inside'],
|
||||
programmeTableStats: {},
|
||||
},
|
||||
graduationInfo: {
|
||||
tableStats: [],
|
||||
graphStats: [],
|
||||
titles: ['', 'All', 'Bachelors', 'Masters', 'Doctors', 'Others'],
|
||||
programmeTableStats: {},
|
||||
},
|
||||
}
|
||||
const transfersToAwayStudyrights = await getTransferredToAndAway(wantedProgrammeCodes, allProgrammeCodes, since)
|
||||
const transferInsideStudyrights = await getTransferredInside(wantedProgrammeCodes, allProgrammeCodes, since)
|
||||
// Started studying in faculty
|
||||
await getFacultyStarters(
|
||||
faculty,
|
||||
transfersToAwayStudyrights,
|
||||
transferInsideStudyrights,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
counts,
|
||||
allBasics,
|
||||
programmeData,
|
||||
programmeFilter,
|
||||
includeAllSpecials
|
||||
)
|
||||
|
||||
// Graduated in faculty
|
||||
await getFacultyGraduates(
|
||||
faculty,
|
||||
transfersToAwayStudyrights,
|
||||
transferInsideStudyrights,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
allBasics,
|
||||
counts,
|
||||
countsGraduations,
|
||||
programmeData,
|
||||
programmeFilter,
|
||||
includeAllSpecials
|
||||
)
|
||||
|
||||
// Transfers
|
||||
await getFacultyTransfers(
|
||||
programmes,
|
||||
wantedProgrammeCodes,
|
||||
allProgrammeCodes,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
allBasics,
|
||||
counts,
|
||||
programmeData,
|
||||
includeAllSpecials
|
||||
)
|
||||
|
||||
// combine tableStats from all categories
|
||||
allBasics.years = years
|
||||
years.forEach(year => {
|
||||
allBasics.studentInfo.tableStats.push([year, ...counts[year]])
|
||||
allBasics.graduationInfo.tableStats.push([year, ...countsGraduations[year]])
|
||||
})
|
||||
allBasics.studentInfo.tableStats.reverse()
|
||||
allBasics.graduationInfo.tableStats.reverse()
|
||||
|
||||
//combine programme level tablestats
|
||||
// all programmes are not present in all data types, check all found programmes and patch missing details with 0
|
||||
|
||||
let allCodes = Object.keys(programmeData.started).concat(
|
||||
Object.keys(programmeData.graduated),
|
||||
Object.keys(programmeData.transferred)
|
||||
)
|
||||
allCodes = [...new Set(allCodes)]
|
||||
|
||||
let studentInfo = {}
|
||||
let graduationInfo = {}
|
||||
|
||||
allCodes.forEach(code => {
|
||||
if (!(code in studentInfo)) studentInfo[code] = {}
|
||||
|
||||
for (const year of years) {
|
||||
// started
|
||||
if (code in programmeData.started) {
|
||||
studentInfo[code][year] = [year, programmeData.started[code][year]]
|
||||
} else {
|
||||
studentInfo[code][year] = [year, 0]
|
||||
}
|
||||
// graduated total
|
||||
if (code in programmeData.graduated) {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat([programmeData.graduated[code][year][0]])
|
||||
if (!(code in graduationInfo)) graduationInfo[code] = {}
|
||||
graduationInfo[code][year] = [year, ...programmeData.graduated[code][year]]
|
||||
} else {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat([0])
|
||||
}
|
||||
// trasferred
|
||||
if (code in programmeData.transferred) {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat(programmeData.transferred[code][year])
|
||||
} else {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat([0, 0, 0])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Object.keys(studentInfo).forEach(prog => {
|
||||
allBasics.studentInfo.programmeTableStats[prog] = [...Object.values(studentInfo[prog])].reverse()
|
||||
})
|
||||
|
||||
Object.keys(graduationInfo).forEach(prog => {
|
||||
allBasics.graduationInfo.programmeTableStats[prog] = Object.values(graduationInfo[prog]).reverse()
|
||||
})
|
||||
|
||||
return allBasics
|
||||
}
|
||||
|
||||
module.exports = { combineFacultyBasics }
|
||||
@@ -0,0 +1,275 @@
|
||||
const { indexOf } = require('lodash')
|
||||
const { mapToProviders } = require('../../util/utils')
|
||||
const {
|
||||
getStatsBasis,
|
||||
defineYear,
|
||||
isMajorStudentCredit,
|
||||
isNonMajorCredit,
|
||||
getYearsArray,
|
||||
getStartDate,
|
||||
isSpecialGroupCredit,
|
||||
} = require('../studyprogrammeHelpers')
|
||||
const {
|
||||
getStudyRights,
|
||||
getCreditsForStudyProgramme,
|
||||
allTransfers,
|
||||
getCourseCodesForStudyProgramme,
|
||||
} = require('../studyprogramme')
|
||||
|
||||
const isFacultyNonMajorCredit = (studyrights, attainment_date, facultyProgrammes) => {
|
||||
let right = ''
|
||||
studyrights.forEach(studyright => {
|
||||
if (studyright) {
|
||||
if (!studyright.graduated && new Date(attainment_date) >= new Date(studyright.studystartdate)) {
|
||||
if (!right) right = studyright.code
|
||||
else if (right && !facultyProgrammes.includes(right) && facultyProgrammes.includes(studyright.code))
|
||||
right = studyright.code
|
||||
} else {
|
||||
if (
|
||||
new Date(attainment_date) >= new Date(studyright.studystartdate) &&
|
||||
new Date(attainment_date) <= new Date(studyright.enddate)
|
||||
) {
|
||||
if (!right) right = studyright.code
|
||||
else if (right && !facultyProgrammes.includes(right) && facultyProgrammes.includes(studyright.code))
|
||||
right = studyright.code
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return facultyProgrammes.includes(right)
|
||||
}
|
||||
|
||||
// Fetches all the credits for the studyprogramme and divides them into major-students, non-major faculty students,
|
||||
// non-major other faculty students adn non-degree student credits
|
||||
// Division is done on the basis that whether the student had a primary studyright to the programme on the attainment_date
|
||||
const getFacultyRegularCreditStats = async ({
|
||||
studyprogramme,
|
||||
since,
|
||||
years,
|
||||
isAcademicYear,
|
||||
includeAllSpecials,
|
||||
facultyProgrammes,
|
||||
}) => {
|
||||
const providercode = mapToProviders([studyprogramme])[0]
|
||||
const courses = await getCourseCodesForStudyProgramme(providercode)
|
||||
const credits = await getCreditsForStudyProgramme(courses, since)
|
||||
const students = [...new Set(credits.map(({ student_studentnumber }) => student_studentnumber))]
|
||||
|
||||
let studyrights = await getStudyRights(students)
|
||||
const transfers = (await allTransfers(studyprogramme, since)).map(t => t.studyrightid).filter(s => s !== 'undefined')
|
||||
if (!includeAllSpecials) {
|
||||
studyrights = studyrights.filter(s => !transfers.includes(s.studyrightid))
|
||||
}
|
||||
|
||||
let majors = getStatsBasis(years)
|
||||
let allNonMajors = getStatsBasis(years)
|
||||
let facultyNonMajors = getStatsBasis(years)
|
||||
let otherNonMajors = getStatsBasis(years)
|
||||
let nonDegree = getStatsBasis(years)
|
||||
|
||||
credits.forEach(({ student_studentnumber, attainment_date, credits }) => {
|
||||
const studentStudyrights = studyrights.filter(studyright => studyright.studentnumber === student_studentnumber)
|
||||
const attainmentYear = defineYear(attainment_date, isAcademicYear)
|
||||
|
||||
if (!includeAllSpecials && isSpecialGroupCredit(studentStudyrights, attainment_date, transfers)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isMajorStudentCredit(studentStudyrights, attainment_date, studyprogramme)) {
|
||||
majors.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
majors.tableStats[attainmentYear] += credits || 0
|
||||
} else if (isNonMajorCredit(studentStudyrights, attainment_date)) {
|
||||
// for comparison
|
||||
allNonMajors.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
allNonMajors.tableStats[attainmentYear] += credits || 0
|
||||
|
||||
if (isFacultyNonMajorCredit(studentStudyrights, attainment_date, facultyProgrammes)) {
|
||||
facultyNonMajors.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
facultyNonMajors.tableStats[attainmentYear] += credits || 0
|
||||
} else {
|
||||
otherNonMajors.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
otherNonMajors.tableStats[attainmentYear] += credits || 0
|
||||
}
|
||||
} else {
|
||||
nonDegree.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
nonDegree.tableStats[attainmentYear] += credits || 0
|
||||
}
|
||||
})
|
||||
|
||||
if (
|
||||
majors.graphStats.every(year => year === 0) &&
|
||||
facultyNonMajors.graphStats.every(year => year === 0) &&
|
||||
otherNonMajors.graphStats.every(year => year === 0) &&
|
||||
nonDegree.graphStats.every(year => year === 0)
|
||||
) {
|
||||
return {
|
||||
majors: { graphStats: [], tableStats: {} },
|
||||
facultyNonMajors: { graphStats: [], tableStats: {} },
|
||||
otherNonMajors: { graphStats: [], tableStats: {} },
|
||||
nonDegree: { graphStats: [], tablestats: {} },
|
||||
}
|
||||
}
|
||||
|
||||
return { majors, facultyNonMajors, otherNonMajors, nonDegree }
|
||||
}
|
||||
|
||||
// Fetches all credits for the studytrack and combines them into statistics
|
||||
// These are for the faculty view which differs from study programme overview
|
||||
// (no transfers, non majors split to in- and outside-faculty)
|
||||
const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgrammes, settings }) => {
|
||||
const { isAcademicYear, includeAllSpecials } = settings
|
||||
const since = getStartDate(studyprogramme, isAcademicYear)
|
||||
const years = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
|
||||
const queryParameters = { studyprogramme, since, years, isAcademicYear, includeAllSpecials, facultyProgrammes }
|
||||
const { majors, facultyNonMajors, otherNonMajors, nonDegree } = await getFacultyRegularCreditStats(queryParameters)
|
||||
|
||||
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
|
||||
const dataFound = [majors, facultyNonMajors, otherNonMajors].some(d => d.graphStats.length) // , transferred
|
||||
|
||||
if (!dataFound) return null
|
||||
|
||||
const tableStats = reversedYears.map(year =>
|
||||
includeAllSpecials
|
||||
? [
|
||||
year,
|
||||
majors.tableStats[year] +
|
||||
facultyNonMajors.tableStats[year] +
|
||||
otherNonMajors.tableStats[year] +
|
||||
nonDegree.tableStats[year],
|
||||
majors.tableStats[year],
|
||||
facultyNonMajors.tableStats[year],
|
||||
otherNonMajors.tableStats[year],
|
||||
nonDegree.tableStats[year],
|
||||
]
|
||||
: [year, majors.tableStats[year], majors.tableStats[year]]
|
||||
)
|
||||
|
||||
const graphStats = includeAllSpecials
|
||||
? [
|
||||
{
|
||||
name: 'Major students credits',
|
||||
data: majors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-major faculty students credits',
|
||||
data: facultyNonMajors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-major other faculty students credits 2',
|
||||
data: otherNonMajors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-degree credits',
|
||||
data: nonDegree.graphStats,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: 'Major students credits',
|
||||
data: majors.graphStats,
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
id: studyprogramme,
|
||||
years,
|
||||
tableStats,
|
||||
graphStats,
|
||||
}
|
||||
}
|
||||
|
||||
const getProgrammeCredits = async (code, yearType, specialGroups, facultyProgrammes) => {
|
||||
const updatedStats = await getFacultyCreditStatsForStudytrack({
|
||||
studyprogramme: code,
|
||||
facultyProgrammes,
|
||||
settings: {
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
})
|
||||
|
||||
return updatedStats
|
||||
}
|
||||
|
||||
const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearType, specialGroups) => {
|
||||
let counts = {}
|
||||
let years = []
|
||||
const titles =
|
||||
specialGroups === 'SPECIAL_INCLUDED'
|
||||
? [
|
||||
'',
|
||||
'Total',
|
||||
'Major students credits',
|
||||
'Non-major faculty students credits',
|
||||
'Non-major other faculty students credits',
|
||||
'Non-degree student credits',
|
||||
]
|
||||
: ['', 'Total', 'Major students credits']
|
||||
|
||||
let allCredits = {
|
||||
id: faculty,
|
||||
years: [],
|
||||
tableStats: [],
|
||||
graphStats: [],
|
||||
programmeTableStats: {},
|
||||
programmeNames: {},
|
||||
titles,
|
||||
}
|
||||
|
||||
const progCodes = allProgrammes.map(p => p.code)
|
||||
for (const prog of programmes) {
|
||||
const data = await getProgrammeCredits(prog.code, yearType, specialGroups, progCodes)
|
||||
if (data) {
|
||||
allCredits.programmeNames[prog.progId] = { code: prog.code, ...prog.name }
|
||||
allCredits.programmeTableStats[prog.progId] = data.tableStats
|
||||
if (!allCredits.lastUpdated || new Date(data.lastUpdated) > new Date(allCredits.lastUpdated))
|
||||
allCredits.lastUpdated = data.lastUpdated
|
||||
|
||||
data.tableStats.forEach(row => {
|
||||
if (!(row[0] in counts)) {
|
||||
counts[row[0]] = row.slice(1)
|
||||
years.push(row[0])
|
||||
} else {
|
||||
counts[row[0]] = row.slice(1).map((value, i) => {
|
||||
return counts[row[0]][i] + value
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// save table stats and graph stats
|
||||
years.forEach(year => {
|
||||
allCredits.tableStats.push([year, ...counts[year]])
|
||||
})
|
||||
|
||||
years.sort()
|
||||
allCredits.years = years
|
||||
|
||||
let majors = []
|
||||
let facultyNonMajor = []
|
||||
let otherNonMajor = []
|
||||
let nonDegree = []
|
||||
|
||||
years.forEach(year => {
|
||||
majors.push(counts[year][1])
|
||||
facultyNonMajor.push(counts[year][2])
|
||||
otherNonMajor.push(counts[year][3])
|
||||
nonDegree.push(counts[year][4])
|
||||
})
|
||||
|
||||
allCredits.graphStats =
|
||||
specialGroups === 'SPECIAL_INCLUDED'
|
||||
? [
|
||||
{ name: 'Major students credits', data: majors },
|
||||
{ name: 'Non-major faculty students credits', data: facultyNonMajor },
|
||||
{ name: 'Non-major other faculty students credits', data: otherNonMajor },
|
||||
{ name: 'Non-degree credits', data: nonDegree },
|
||||
]
|
||||
: [{ name: 'Major students credits', data: majors }]
|
||||
|
||||
return allCredits
|
||||
}
|
||||
|
||||
module.exports = { combineFacultyCredits, getFacultyCreditStatsForStudytrack }
|
||||
@@ -0,0 +1,389 @@
|
||||
const moment = require('moment')
|
||||
const {
|
||||
graduatedStudyrights,
|
||||
studyrightsByRightStartYear,
|
||||
bachelorStudyright,
|
||||
hasMasterRight,
|
||||
statutoryAbsences,
|
||||
} = require('./faculty')
|
||||
const { findRightProgramme, isNewProgramme } = require('./facultyHelpers')
|
||||
const { getYearsArray, getYearsObject, getMean, getMedian, defineYear } = require('../studyprogrammeHelpers')
|
||||
const { codes } = require('../../../config/programmeCodes')
|
||||
|
||||
const sortProgrammes = data => {
|
||||
const check = name => {
|
||||
if (isNaN(name[0])) return -1
|
||||
return 1
|
||||
}
|
||||
data.sort((a, b) => {
|
||||
if (check(a.name) === check(b.name)) return a.name.localeCompare(b.name)
|
||||
return check(a.name) - check(b.name)
|
||||
})
|
||||
}
|
||||
|
||||
const getSortedProgIds = progs => {
|
||||
let programmes = []
|
||||
|
||||
for (const prog of progs) {
|
||||
if (Object.keys(codes).includes(prog)) {
|
||||
programmes = [...programmes, { name: codes[prog].toUpperCase(), code: prog }]
|
||||
} else {
|
||||
programmes = [...programmes, { name: prog, code: prog }]
|
||||
}
|
||||
}
|
||||
sortProgrammes(programmes)
|
||||
return programmes
|
||||
}
|
||||
|
||||
const findBachelorStartdate = async id => {
|
||||
const studyright = await bachelorStudyright(id)
|
||||
if (studyright) return studyright.studystartdate
|
||||
return null
|
||||
}
|
||||
|
||||
const getProgrammeObjectBasis = (years, levels, emptyObject = true) => {
|
||||
return levels.reduce(
|
||||
(acc, level) => ({ ...acc, [level]: years.reduce((acc, year) => ({ ...acc, [year]: emptyObject ? {} : 0 }), {}) }),
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
const getStatutoryAbsences = async (studentnumber, startdate, enddate) => {
|
||||
const absences = await statutoryAbsences(studentnumber, startdate, enddate)
|
||||
if (absences.length) {
|
||||
const absentMonths = absences.reduce((sum, ab) => sum + moment(ab.end).diff(moment(ab.start), 'months'), 0)
|
||||
return absentMonths
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const countTimeCategories = (times, goal) => {
|
||||
const statistics = { onTime: 0, yearOver: 0, wayOver: 0 }
|
||||
times.forEach(time => {
|
||||
if (time <= goal) statistics.onTime += 1
|
||||
else if (time <= goal + 12) statistics.yearOver += 1
|
||||
else statistics.wayOver += 1
|
||||
})
|
||||
return statistics
|
||||
}
|
||||
|
||||
const hasMS = async (programme, elements, studyrightid) => {
|
||||
// Only bachelor studyright: socCom, kasvatustiede - varhaiskasvatus track, farmasia - applied only to Bc = farmaseutti,
|
||||
if (['KH74_001', '620050-ba', '620030-ba', '520091-ba'].includes(programme)) {
|
||||
return false
|
||||
}
|
||||
if (programme === 'KH60_001') {
|
||||
return !elements.some(el => ['EDUK-VO', '0371', '620050-ba', '620030-ba'].includes(el.dataValues.code))
|
||||
}
|
||||
if (programme === 'KH55_001') {
|
||||
const result = await hasMasterRight(studyrightid.replace(/-1$/, '-2'))
|
||||
if (!result) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const addGraduation = async (
|
||||
extentcode,
|
||||
startdate,
|
||||
studyrightid,
|
||||
enddate,
|
||||
studentnumber,
|
||||
graduationAmounts,
|
||||
graduationTimes,
|
||||
year,
|
||||
programmes,
|
||||
programme
|
||||
) => {
|
||||
let actualStartdate = startdate
|
||||
let level = null
|
||||
|
||||
if (programme === 'MH30_001' || programme === 'MH30_003') {
|
||||
level = 'bcMsCombo'
|
||||
} else if (extentcode === 1) {
|
||||
level = 'bachelor'
|
||||
} else if (extentcode === 2) {
|
||||
if (studyrightid.slice(-2) === '-2') {
|
||||
level = 'bcMsCombo'
|
||||
actualStartdate = await findBachelorStartdate(studyrightid.replace(/-2$/, '-1'))
|
||||
} else {
|
||||
level = 'master'
|
||||
}
|
||||
} else if (extentcode === 4) {
|
||||
level = 'doctor'
|
||||
} else if (extentcode === 3) {
|
||||
level = 'licentiate'
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
const absoluteTimeToGraduation = moment(enddate).diff(moment(actualStartdate), 'months')
|
||||
const statutoryAbsences = await getStatutoryAbsences(studentnumber, actualStartdate, enddate)
|
||||
const timeToGraduation = absoluteTimeToGraduation - statutoryAbsences
|
||||
|
||||
graduationAmounts[level][year] += 1
|
||||
graduationTimes[level][year] = [...graduationTimes[level][year], timeToGraduation]
|
||||
|
||||
if (!(programme in programmes[level][year]))
|
||||
programmes[level][year][programme] = { graduationTimes: [], graduationAmounts: 0 }
|
||||
|
||||
programmes[level][year][programme].graduationAmounts += 1
|
||||
programmes[level][year][programme].graduationTimes = [
|
||||
...programmes[level][year][programme].graduationTimes,
|
||||
timeToGraduation,
|
||||
]
|
||||
}
|
||||
|
||||
const getClassSizes = async (faculty, since, classSizes, programmeFilter, years) => {
|
||||
const studyrights = await studyrightsByRightStartYear(faculty, since, [0, 1])
|
||||
|
||||
// get all received studyrights for each year
|
||||
// a transferred student is counted into new programmes class size i.e.
|
||||
// students who received studyright on year X and graduated/will be graduating from programme Y
|
||||
// if we only counted those who started fresh in the new programme we could get a bigger number of graduates
|
||||
// than the class size, as the graduatation time count only looks at a degree as whole studyright
|
||||
for (const right of studyrights) {
|
||||
const { startdate, studyrightid, extentcode, studyrightElements } = right
|
||||
const { programme } = findRightProgramme(studyrightElements, 'graduated')
|
||||
if (programmeFilter === 'NEW_STUDY_PROGRAMMES' && !isNewProgramme(programme)) continue
|
||||
|
||||
let actualStartdate = startdate
|
||||
let level = null
|
||||
|
||||
if (programme === 'MH30_001' || programme === 'MH30_003') {
|
||||
level = 'bcMsCombo'
|
||||
} else if (extentcode === 1) {
|
||||
level = 'bachelor'
|
||||
} else if (extentcode === 2) {
|
||||
if (studyrightid.slice(-2) === '-2') {
|
||||
level = 'bcMsCombo'
|
||||
actualStartdate = await findBachelorStartdate(studyrightid.replace(/-2$/, '-1'))
|
||||
} else {
|
||||
level = 'master'
|
||||
}
|
||||
} else if (extentcode === 4) {
|
||||
level = 'doctor'
|
||||
} else if (extentcode === 3) {
|
||||
level = 'licentiate'
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
const startYear = defineYear(actualStartdate, false)
|
||||
|
||||
// On faculty level, count started bachelor rights to BcMsCombo as well --> useful class size even if master studies have yet not started
|
||||
// On programme level BcMsCombo sizes are iffy as many students haven't started masters yet --> no Ms programme element to count
|
||||
if (level !== 'bcMsCombo') {
|
||||
classSizes[level][startYear] += 1
|
||||
if (level === 'bachelor' && (await hasMS(programme, studyrightElements, studyrightid))) {
|
||||
classSizes['bcMsCombo'][startYear] += 1
|
||||
}
|
||||
}
|
||||
|
||||
if (!(programme in classSizes.programmes)) {
|
||||
if (extentcode === 2) {
|
||||
classSizes.programmes[programme] = {}
|
||||
classSizes.programmes[programme]['bcMsCombo'] = getYearsObject({ years, emptyArrays: false })
|
||||
classSizes.programmes[programme]['master'] = getYearsObject({ years, emptyArrays: false })
|
||||
} else {
|
||||
classSizes.programmes[programme] = getYearsObject({ years, emptyArrays: false })
|
||||
}
|
||||
}
|
||||
|
||||
if (extentcode === 2) {
|
||||
classSizes.programmes[programme][level][startYear] += 1
|
||||
} else {
|
||||
classSizes.programmes[programme][startYear] += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const count = async (faculty, since, years, yearsList, levels, programmeFilter, programmeNames, goals, mode) => {
|
||||
let graduationAmounts = {}
|
||||
let graduationTimes = {}
|
||||
let programmes = getProgrammeObjectBasis(yearsList, levels)
|
||||
let data = {
|
||||
means: {},
|
||||
medians: {},
|
||||
programmes: {
|
||||
medians: getProgrammeObjectBasis(yearsList, levels),
|
||||
means: getProgrammeObjectBasis(yearsList, levels),
|
||||
},
|
||||
}
|
||||
|
||||
levels.forEach(level => {
|
||||
graduationAmounts[level] = getYearsObject({ years: yearsList })
|
||||
graduationTimes[level] = getYearsObject({ years: yearsList, emptyArrays: true })
|
||||
data.medians[level] = []
|
||||
data.means[level] = []
|
||||
})
|
||||
|
||||
let studyrights = null
|
||||
if (mode === 'gradYear') {
|
||||
studyrights = await graduatedStudyrights(faculty, since)
|
||||
} else {
|
||||
studyrights = await studyrightsByRightStartYear(faculty, since)
|
||||
}
|
||||
|
||||
for (const right of studyrights) {
|
||||
const { enddate, startdate, studyrightid, extentcode, studyrightElements, studentnumber } = right
|
||||
const { programme, programmeName } = findRightProgramme(studyrightElements, 'graduated')
|
||||
if (programmeFilter === 'NEW_STUDY_PROGRAMMES' && !isNewProgramme(programme)) continue
|
||||
|
||||
if (!(programme in programmeNames)) {
|
||||
programmeNames[programme] = programmeName
|
||||
}
|
||||
|
||||
let year = null
|
||||
if (mode === 'gradYear') {
|
||||
year = defineYear(enddate, false)
|
||||
} else {
|
||||
year = defineYear(startdate, false)
|
||||
}
|
||||
|
||||
await addGraduation(
|
||||
extentcode,
|
||||
startdate,
|
||||
studyrightid,
|
||||
enddate,
|
||||
studentnumber,
|
||||
graduationAmounts,
|
||||
graduationTimes,
|
||||
year,
|
||||
programmes,
|
||||
programme
|
||||
)
|
||||
}
|
||||
|
||||
years.forEach(year => {
|
||||
levels.forEach(level => {
|
||||
const median = getMedian(graduationTimes[level][year])
|
||||
const mean = getMean(graduationTimes[level][year])
|
||||
const statistics = { onTime: 0, yearOver: 0, wayOver: 0 }
|
||||
|
||||
// Programme level breakdown
|
||||
data.programmes.medians[level][year] = { programmes: [], data: [] }
|
||||
data.programmes.means[level][year] = { programmes: [], data: [] }
|
||||
const programmeCodes = Object.keys(programmes[level][year])
|
||||
const programmeIds = getSortedProgIds(programmeCodes)
|
||||
|
||||
if (programmeIds.length > 0) {
|
||||
for (const prog of programmeIds) {
|
||||
const progMedian = getMedian(programmes[level][year][prog.code].graduationTimes)
|
||||
const progMean = getMean(programmes[level][year][prog.code].graduationTimes)
|
||||
|
||||
let goal = goals[level]
|
||||
if (faculty === 'H30' && Object.keys(goals.exceptions).includes(prog.code)) {
|
||||
goal += goals.exceptions[prog.code]
|
||||
}
|
||||
|
||||
const progStatistics = countTimeCategories(programmes[level][year][prog.code].graduationTimes, goal)
|
||||
Object.keys(progStatistics).forEach(key => (statistics[key] += progStatistics[key]))
|
||||
|
||||
data.programmes.medians[level][year].programmes = [
|
||||
...data.programmes.medians[level][year].programmes,
|
||||
prog.code,
|
||||
]
|
||||
data.programmes.medians[level][year].data = [
|
||||
...data.programmes.medians[level][year].data,
|
||||
{
|
||||
y: progMedian,
|
||||
amount: programmes[level][year][prog.code].graduationAmounts,
|
||||
statistics: progStatistics,
|
||||
name: prog.name,
|
||||
code: prog.code,
|
||||
},
|
||||
]
|
||||
|
||||
data.programmes.means[level][year].programmes = [...data.programmes.means[level][year].programmes, prog.code]
|
||||
data.programmes.means[level][year].data = [
|
||||
...data.programmes.means[level][year].data,
|
||||
{
|
||||
y: progMean,
|
||||
amount: programmes[level][year][prog.code].graduationAmounts,
|
||||
statistics: progStatistics,
|
||||
name: prog.name,
|
||||
code: prog.code,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
data.medians[level] = [
|
||||
...data.medians[level],
|
||||
{ y: median, amount: graduationAmounts[level][year], statistics, name: year },
|
||||
]
|
||||
data.means[level] = [
|
||||
...data.means[level],
|
||||
{ y: mean, amount: graduationAmounts[level][year], statistics, name: year },
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
const countGraduationTimes = async (faculty, programmeFilter) => {
|
||||
const isAcademicYear = false // = yearType === 'ACADEMIC_YEAR'
|
||||
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
|
||||
const yearsList = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
const levels = ['bachelor', 'bcMsCombo', 'master', 'doctor', 'licentiate']
|
||||
const programmeNames = {}
|
||||
|
||||
const years = [...yearsList].reverse()
|
||||
const goals = {
|
||||
bachelor: 36,
|
||||
bcMsCombo: 60,
|
||||
master: 24,
|
||||
doctor: 48,
|
||||
licentiate: 78,
|
||||
exceptions: {
|
||||
MH30_004: 6, //months more
|
||||
'420420-ma': 6,
|
||||
MH30_001: 12,
|
||||
'320011-ma': 12,
|
||||
'320001-ma': 12,
|
||||
MH30_003: 12,
|
||||
'320002-ma': 12,
|
||||
},
|
||||
}
|
||||
|
||||
if (faculty === 'H90') {
|
||||
goals.bcMsCombo += 12
|
||||
}
|
||||
|
||||
// We count studyrights (vs. studyright_elements)
|
||||
// This way we get the whole time for a degree, even if the student was transferred to a new programme
|
||||
// E.g. started 8/2016 in old Bc, transferred to new 10/2020, graduated from new 1/2021 --> total 53 months (not 3)
|
||||
|
||||
const byGradYear = await count(
|
||||
faculty,
|
||||
since,
|
||||
years,
|
||||
yearsList,
|
||||
levels,
|
||||
programmeFilter,
|
||||
programmeNames,
|
||||
goals,
|
||||
'gradYear'
|
||||
)
|
||||
const byStartYear = await count(
|
||||
faculty,
|
||||
since,
|
||||
years,
|
||||
yearsList,
|
||||
levels,
|
||||
programmeFilter,
|
||||
programmeNames,
|
||||
goals,
|
||||
'startYear'
|
||||
)
|
||||
|
||||
const classSizes = getProgrammeObjectBasis(yearsList, levels, false)
|
||||
classSizes['programmes'] = {}
|
||||
await getClassSizes(faculty, since, classSizes, programmeFilter, years)
|
||||
|
||||
return { id: faculty, goals, byGradYear, byStartYear, programmeNames, classSizes }
|
||||
}
|
||||
|
||||
module.exports = { countGraduationTimes }
|
||||
@@ -0,0 +1,151 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { Op } = Sequelize
|
||||
const { codes } = require('../../../config/programmeCodes')
|
||||
|
||||
const findRightProgramme = (studyrightElements, mode) => {
|
||||
let programme = ''
|
||||
let programmeName = ''
|
||||
let studyRightElement = null
|
||||
|
||||
if (studyrightElements) {
|
||||
if (mode === 'started') {
|
||||
studyRightElement = studyrightElements
|
||||
.filter(sre => sre.element_detail.type === 20)
|
||||
.sort((a, b) => new Date(a.startdate) - new Date(b.startdate))[0]
|
||||
// this way round counts to old programmes, other way around to new programmes
|
||||
} else {
|
||||
// graduated
|
||||
studyRightElement = studyrightElements
|
||||
.filter(sre => sre.element_detail.type === 20)
|
||||
.sort((a, b) => new Date(b.startdate) - new Date(a.startdate))[0]
|
||||
}
|
||||
if (studyRightElement) {
|
||||
programme = studyRightElement.code
|
||||
programmeName = studyRightElement.element_detail.name
|
||||
}
|
||||
}
|
||||
return { programme, programmeName }
|
||||
}
|
||||
|
||||
const facultyFormatStudyright = studyright => {
|
||||
const {
|
||||
studyrightid,
|
||||
studystartdate,
|
||||
enddate,
|
||||
givendate,
|
||||
graduated,
|
||||
active,
|
||||
prioritycode,
|
||||
extentcode,
|
||||
student,
|
||||
studyright_elements,
|
||||
startdate,
|
||||
} = studyright
|
||||
|
||||
return {
|
||||
studyrightid,
|
||||
studystartdate,
|
||||
startdate,
|
||||
enddate,
|
||||
givendate,
|
||||
graduated,
|
||||
active,
|
||||
prioritycode,
|
||||
extentcode,
|
||||
studentnumber: student.studentnumber,
|
||||
studyrightElements: studyright_elements,
|
||||
}
|
||||
}
|
||||
|
||||
const formatFacultyTransfer = transfer => {
|
||||
const { sourcecode, targetcode, transferdate, studyrightid } = transfer
|
||||
return {
|
||||
sourcecode,
|
||||
targetcode,
|
||||
transferdate,
|
||||
studyrightid,
|
||||
}
|
||||
}
|
||||
|
||||
const facultyFormatProgramme = programme => {
|
||||
const { code, name } = programme
|
||||
return { code, name }
|
||||
}
|
||||
|
||||
const formatFacultyThesisWriter = credit => {
|
||||
const { course_code, credits, attainment_date, student_studentnumber, course } = credit
|
||||
return {
|
||||
course_code,
|
||||
credits,
|
||||
attainment_date,
|
||||
student_studentnumber,
|
||||
courseUnitType: course.course_unit_type,
|
||||
}
|
||||
}
|
||||
|
||||
const formatOrganization = org => {
|
||||
const { id, name, code, parent_id } = org
|
||||
return { id, name, code, parentId: parent_id }
|
||||
}
|
||||
|
||||
const formatAbsence = absence => {
|
||||
const { semestercode, semester } = absence
|
||||
return { semestercode, start: semester.startdate, end: semester.enddate }
|
||||
}
|
||||
|
||||
const newProgrammes = [/^KH/, /^MH/, /^T/, /^LI/, /^K-/, /^FI/, /^00901$/, /^00910$/]
|
||||
|
||||
const isNewProgramme = code => {
|
||||
for (let i = 0; i < newProgrammes.length; i++) {
|
||||
if (newProgrammes[i].test(code)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const checkTransfers = (s, insideTransfersStudyrights, transfersToOrAwayStudyrights) => {
|
||||
if (transfersToOrAwayStudyrights.includes(s.studyrightid)) return true
|
||||
if (insideTransfersStudyrights.includes(s.studyrightid) && new Date(s.studystartdate) < new Date('2017-08-01'))
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
const getExtentFilter = includeAllSpecials => {
|
||||
const filteredExtents = [16] // always filter out secondary subject students
|
||||
if (!includeAllSpecials) {
|
||||
filteredExtents.push(7, 34, 22, 99, 14, 13, 9)
|
||||
}
|
||||
let studyrightWhere = {
|
||||
extentcode: {
|
||||
[Op.notIn]: filteredExtents,
|
||||
},
|
||||
}
|
||||
return studyrightWhere
|
||||
}
|
||||
|
||||
const mapCodesToIds = data => {
|
||||
// Add programme id e.g. TKT
|
||||
const keys = Object.keys(codes)
|
||||
for (const prog of data) {
|
||||
if (keys.includes(prog.code)) {
|
||||
prog.progId = codes[prog.code].toUpperCase()
|
||||
} else {
|
||||
prog.progId = prog.code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
findRightProgramme,
|
||||
facultyFormatStudyright,
|
||||
facultyFormatProgramme,
|
||||
formatFacultyTransfer,
|
||||
formatFacultyThesisWriter,
|
||||
formatOrganization,
|
||||
formatAbsence,
|
||||
isNewProgramme,
|
||||
checkTransfers,
|
||||
getExtentFilter,
|
||||
mapCodesToIds,
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
const { redisClient } = require('../redis')
|
||||
const moment = require('moment')
|
||||
|
||||
const createRedisKeyForFacultyProgrammes = (id, programmeFilter) => `FACULTY_PROGRAMMES_${id}_${programmeFilter}`
|
||||
const createRedisKeyForBasicStats = (id, yearType, programmeFilter, specialGroups) =>
|
||||
`FACULTY_BASIC_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
|
||||
const createRedisKeyForCreditStats = (id, yearType, programmeFilter, specialGroups) =>
|
||||
`FACULTY_CREDIT_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
|
||||
const createRedisKeyForThesiswriters = (id, yearType, programmeFilter, specialGroups) =>
|
||||
`FACULTY_THESIS_WRITERS_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
|
||||
const createRedisKeyForGraduationTimeStats = (id, programmeFilter) =>
|
||||
`FACULTY_GRADUATION_TIME_STATS_${id}_${programmeFilter}`
|
||||
const createRediskeyForFacultyProgress = (id, special_groups, graduated) =>
|
||||
`FACULTY_PROGRESS_STATS_${id}_${special_groups}_${graduated}`
|
||||
const createRediskeyForFacultyStudents = (id, special_groups, graduated) =>
|
||||
`FACULTY_STUDENTS_STATS_${id}_${special_groups}_${graduated}`
|
||||
|
||||
const setFacultyProgrammes = async (id, data, programmeFilter) => {
|
||||
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
|
||||
const dataToRedis = {
|
||||
data: data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getFacultyProgrammes = async (id, programmeFilter) => {
|
||||
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setBasicStats = async (data, yearType, programmeFilter, specialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForBasicStats(id, yearType, programmeFilter, 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
|
||||
}
|
||||
|
||||
const getBasicStats = async (id, yearType, programmeFilter, specialGroups) => {
|
||||
const redisKey = createRedisKeyForBasicStats(id, yearType, programmeFilter, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setCreditStats = async (data, yearType, programmeFilter, specialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter, 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
|
||||
}
|
||||
|
||||
const getCreditStats = async (id, yearType, programmeFilter, specialGroups) => {
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setThesisWritersStats = async (data, yearType, programmeFilter, specialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForThesiswriters(id, yearType, programmeFilter, 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
|
||||
}
|
||||
const getThesisWritersStats = async (id, yearType, programmeFilter, specialGroups) => {
|
||||
const redisKey = createRedisKeyForThesiswriters(id, yearType, programmeFilter, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setGraduationStats = async (data, programmeFilter) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForGraduationTimeStats(id, programmeFilter)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getGraduationStats = async (id, programmeFilter) => {
|
||||
const redisKey = createRedisKeyForGraduationTimeStats(id, programmeFilter)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const getFacultyProgressStats = async (id, specialGroups, graduated) => {
|
||||
const redisKey = createRediskeyForFacultyProgress(id, specialGroups, graduated)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setFacultyProgressStats = async (data, specialGroups, graduated) => {
|
||||
const { id } = data
|
||||
const redisKey = createRediskeyForFacultyProgress(id, specialGroups, graduated)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getFacultyStudentStats = async (id, specialGroups, graduated) => {
|
||||
const redisKey = createRediskeyForFacultyStudents(id, specialGroups, graduated)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setFacultyStudentStats = async (data, specialGroups, graduated) => {
|
||||
const { id } = data
|
||||
const redisKey = createRediskeyForFacultyStudents(id, specialGroups, graduated)
|
||||
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 = {
|
||||
setFacultyProgrammes,
|
||||
getFacultyProgrammes,
|
||||
setBasicStats,
|
||||
getBasicStats,
|
||||
setCreditStats,
|
||||
getCreditStats,
|
||||
setThesisWritersStats,
|
||||
getThesisWritersStats,
|
||||
setGraduationStats,
|
||||
getGraduationStats,
|
||||
getFacultyProgressStats,
|
||||
setFacultyProgressStats,
|
||||
getFacultyStudentStats,
|
||||
setFacultyStudentStats,
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
const { indexOf } = require('lodash')
|
||||
const moment = require('moment')
|
||||
const { getAcademicYearDates } = require('../../util/semester')
|
||||
const {
|
||||
getCreditThresholds,
|
||||
getBachelorCreditGraphStats,
|
||||
getMasterCreditGraphStats,
|
||||
getDoctoralCreditGraphStats,
|
||||
getYearsArray,
|
||||
getCorrectStudentnumbers,
|
||||
tableTitles,
|
||||
getOnlyMasterCreditGraphStats,
|
||||
getOnlyMasterThresholds,
|
||||
} = require('../studyprogrammeHelpers')
|
||||
const { studytrackStudents, allStudyrights } = require('../studyprogramme')
|
||||
|
||||
const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts, limits, limitKeys, prog) => {
|
||||
let data = {}
|
||||
let programmeData = {}
|
||||
thresholdKeys.forEach(t => (data[t] = 0))
|
||||
limitKeys.forEach(t => (programmeData[t] = 0))
|
||||
students.forEach(({ credits }) => {
|
||||
const creditcount = credits
|
||||
.filter(credit => moment(credit.attainment_date).isAfter(startDate))
|
||||
.reduce((prev, curr) => (prev += curr.credits), 0)
|
||||
|
||||
data[thresholdKeys[0]] += creditcount < thresholdAmounts[0] ? 1 : 0
|
||||
data[thresholdKeys[1]] += creditcount >= thresholdAmounts[0] && creditcount < thresholdAmounts[1] ? 1 : 0
|
||||
data[thresholdKeys[2]] += creditcount >= thresholdAmounts[1] && creditcount < thresholdAmounts[2] ? 1 : 0
|
||||
data[thresholdKeys[3]] += creditcount >= thresholdAmounts[2] && creditcount < thresholdAmounts[3] ? 1 : 0
|
||||
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] && creditcount < thresholdAmounts[4] ? 1 : 0
|
||||
if (thresholdKeys.length === 8) {
|
||||
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] && creditcount < thresholdAmounts[5] ? 1 : 0
|
||||
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[5] && creditcount < thresholdAmounts[6] ? 1 : 0
|
||||
data[thresholdKeys[7]] += creditcount >= thresholdAmounts[6] ? 1 : 0
|
||||
} else if (thresholdKeys.length === 7) {
|
||||
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] && creditcount < thresholdAmounts[5] ? 1 : 0
|
||||
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[5] ? 1 : 0
|
||||
} else {
|
||||
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] ? 1 : 0
|
||||
}
|
||||
if (['KH', 'MH'].includes(prog)) {
|
||||
programmeData[limitKeys[0]] += creditcount <= limits[5][0] ? 1 : 0
|
||||
programmeData[limitKeys[1]] += limits[4][0] <= creditcount && limits[4][1] > creditcount ? 1 : 0
|
||||
programmeData[limitKeys[2]] += limits[3][0] <= creditcount && limits[3][1] > creditcount ? 1 : 0
|
||||
programmeData[limitKeys[3]] += limits[2][0] <= creditcount && limits[2][1] > creditcount ? 1 : 0
|
||||
programmeData[limitKeys[4]] += limits[1][0] <= creditcount && limits[1][1] > creditcount ? 1 : 0
|
||||
programmeData[limitKeys[5]] += creditcount >= limits[0][0] ? 1 : 0
|
||||
}
|
||||
})
|
||||
|
||||
return [data, programmeData]
|
||||
}
|
||||
|
||||
const combineFacultyStudentProgress = async (faculty, programmes, specialGroups, graduated) => {
|
||||
const since = new Date('2017-08-01')
|
||||
const isAcademicYear = true
|
||||
const includeYearsCombined = true
|
||||
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
|
||||
const includeGraduated = graduated === 'GRADUATED_INCLUDED'
|
||||
const yearsArray = getYearsArray(since.getFullYear(), isAcademicYear, includeYearsCombined)
|
||||
|
||||
let allBachelorGraphStats = getBachelorCreditGraphStats(yearsArray)
|
||||
let allBsMsGraphstats = getMasterCreditGraphStats(yearsArray)
|
||||
let allMastersGraphStats = getOnlyMasterCreditGraphStats(yearsArray)
|
||||
let allDoctoralGraphStats = getDoctoralCreditGraphStats(yearsArray)
|
||||
|
||||
let bachelorsProgrammeStats = {}
|
||||
let bcMsProgrammeStats = {}
|
||||
let mastersProgrammeStats = {}
|
||||
let doctoralProgrammeStats = {}
|
||||
|
||||
let bachelorsTableStats = new Array(yearsArray.length)
|
||||
let bachelorMastersTableStats = new Array(yearsArray.length)
|
||||
let mastersTableStats = new Array(yearsArray.length)
|
||||
let doctoralTableStats = new Array(yearsArray.length)
|
||||
|
||||
let yearlyBachelorTitles = []
|
||||
let yearlyBcMsTitles = []
|
||||
let yearlyMasterTitles = []
|
||||
|
||||
const reversedYears = [...yearsArray].reverse()
|
||||
reversedYears.forEach(year => {
|
||||
bachelorsTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
bachelorMastersTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
mastersTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0]
|
||||
doctoralTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
})
|
||||
const limitKeys = ['zero', 't5', 't4', 't3', 't2', 't1']
|
||||
let bachelorlimits = []
|
||||
let masterlimits = []
|
||||
let bcmslimits = []
|
||||
for (const year of reversedYears) {
|
||||
const { startDate, endDate } = getAcademicYearDates(year, since)
|
||||
const today = moment()
|
||||
const lastDayOfMonth = moment(today).endOf('month')
|
||||
let months = Math.round(moment.duration(moment(lastDayOfMonth).diff(moment(startDate))).asMonths())
|
||||
|
||||
if (months >= 36) {
|
||||
bachelorlimits = [
|
||||
[Math.ceil(36 * (60 / 12)), null],
|
||||
[Math.ceil(36 * (45 / 12)), Math.ceil(36 * (60 / 12))],
|
||||
[Math.ceil(36 * (30 / 12)), Math.ceil(36 * (45 / 12))],
|
||||
[Math.ceil(36 * (15 / 12)), Math.ceil(36 * (30 / 12))],
|
||||
[1, Math.ceil(36 * (15 / 12))],
|
||||
[0, null],
|
||||
]
|
||||
} else {
|
||||
bachelorlimits = [
|
||||
[Math.ceil(months * (60 / 12)), null],
|
||||
[Math.ceil(months * (45 / 12)), Math.ceil(months * (60 / 12))],
|
||||
[Math.ceil(months * (30 / 12)), Math.ceil(months * (45 / 12))],
|
||||
[Math.ceil(months * (15 / 12)), Math.ceil(months * (30 / 12))],
|
||||
[1, Math.ceil(months * (15 / 12))],
|
||||
[0, null],
|
||||
]
|
||||
}
|
||||
if (months >= 24) {
|
||||
masterlimits = [
|
||||
[Math.ceil(24 * (60 / 12)), null],
|
||||
[Math.ceil(24 * (45 / 12)), Math.ceil(24 * (60 / 12))],
|
||||
[Math.ceil(24 * (30 / 12)), Math.ceil(24 * (45 / 12))],
|
||||
[Math.ceil(24 * (15 / 12)), Math.ceil(24 * (30 / 12))],
|
||||
[1, Math.ceil(20 * (15 / 12))],
|
||||
[0, null],
|
||||
]
|
||||
} else {
|
||||
masterlimits = [
|
||||
[Math.ceil(months * (60 / 12)), null],
|
||||
[Math.ceil(months * (45 / 12)), Math.ceil(months * (60 / 12))],
|
||||
[Math.ceil(months * (30 / 12)), Math.ceil(months * (45 / 12))],
|
||||
[Math.ceil(months * (15 / 12)), Math.ceil(months * (30 / 12))],
|
||||
[1, Math.ceil(months * (15 / 12))],
|
||||
[0, null],
|
||||
]
|
||||
}
|
||||
if (months >= 60) {
|
||||
bcmslimits = [
|
||||
[Math.ceil(60 * (60 / 12)) + 180, null],
|
||||
[Math.ceil(60 * (45 / 12)) + 180, Math.ceil(60 * (60 / 12)) + 180],
|
||||
[Math.ceil(60 * (30 / 12)) + 180, Math.ceil(60 * (45 / 12)) + 180],
|
||||
[Math.ceil(60 * (15 / 12)) + 180, Math.ceil(60 * (30 / 12)) + 180],
|
||||
[181, Math.ceil(60 * (15 / 12)) + 180],
|
||||
[180, null],
|
||||
]
|
||||
} else {
|
||||
bcmslimits = [
|
||||
[Math.ceil(months * (60 / 12)) + 180, null],
|
||||
[Math.ceil(months * (45 / 12)) + 180, Math.ceil(months * (60 / 12)) + 180],
|
||||
[Math.ceil(months * (30 / 12)) + 180, Math.ceil(months * (45 / 12)) + 180],
|
||||
[Math.ceil(months * (15 / 12)) + 180, Math.ceil(months * (30 / 12)) + 180],
|
||||
[181, Math.ceil(months * (15 / 12)) + 180],
|
||||
[180, null],
|
||||
]
|
||||
}
|
||||
|
||||
yearlyBachelorTitles.push([
|
||||
'0 Credits',
|
||||
`${bachelorlimits[4][0]} ≤ Credits < ${bachelorlimits[4][1]}`,
|
||||
`${bachelorlimits[3][0]} ≤ Credits < ${bachelorlimits[3][1]}`,
|
||||
`${bachelorlimits[2][0]} ≤ Credits < ${bachelorlimits[2][1]}`,
|
||||
`${bachelorlimits[1][0]} ≤ Credits < ${bachelorlimits[1][1]}`,
|
||||
`${bachelorlimits[0][0]} ≤ Credits`,
|
||||
])
|
||||
yearlyMasterTitles.push([
|
||||
'0 Credits',
|
||||
`${masterlimits[4][0]} ≤ Credits < ${masterlimits[4][1]}`,
|
||||
`${masterlimits[3][0]} ≤ Credits < ${masterlimits[3][1]}`,
|
||||
`${masterlimits[2][0]} ≤ Credits < ${masterlimits[2][1]}`,
|
||||
`${masterlimits[1][0]} ≤ Credits < ${masterlimits[1][1]}`,
|
||||
`${masterlimits[0][0]} ≤ Credits`,
|
||||
])
|
||||
yearlyBcMsTitles.push([
|
||||
'180 Credits',
|
||||
`${bcmslimits[4][0]} ≤ Credits < ${bcmslimits[4][1]}`,
|
||||
`${bcmslimits[3][0]} ≤ Credits < ${bcmslimits[3][1]}`,
|
||||
`${bcmslimits[2][0]} ≤ Credits < ${bcmslimits[2][1]}`,
|
||||
`${bcmslimits[1][0]} ≤ Credits < ${bcmslimits[1][1]}`,
|
||||
`${bcmslimits[0][0]} ≤ Credits`,
|
||||
])
|
||||
for (const { progId, code } of programmes) {
|
||||
let { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(code)
|
||||
if (!creditThresholdKeys) return
|
||||
const studentnumbers = await getCorrectStudentnumbers({
|
||||
codes: [code],
|
||||
startDate,
|
||||
endDate,
|
||||
includeAllSpecials,
|
||||
includeGraduated,
|
||||
})
|
||||
|
||||
const all = await allStudyrights(code, studentnumbers)
|
||||
const students = await studytrackStudents(studentnumbers)
|
||||
|
||||
if (code.includes('KH')) {
|
||||
const data = getStudentData(
|
||||
startDate,
|
||||
students,
|
||||
creditThresholdKeys,
|
||||
creditThresholdAmounts,
|
||||
bachelorlimits,
|
||||
limitKeys,
|
||||
'KH'
|
||||
)
|
||||
const studentData = data[0]
|
||||
const programmeData = data[1]
|
||||
if (!(progId in bachelorsProgrammeStats)) {
|
||||
bachelorsProgrammeStats[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
bachelorsTableStats[indexOf(reversedYears, year)][1] += all.length || 0
|
||||
if (year !== 'Total') {
|
||||
bachelorsProgrammeStats[progId][indexOf(reversedYears, year)] = limitKeys.map(key => programmeData[key])
|
||||
}
|
||||
for (const key of Object.keys(studentData)) {
|
||||
bachelorsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
studentData[key] || 0
|
||||
allBachelorGraphStats[key].data[indexOf(yearsArray, year)] += studentData[key] || 0
|
||||
}
|
||||
} else if (code.includes('MH')) {
|
||||
const all_BcMs = all.filter(sr => sr.studyrightid.slice(-2) === '-2')
|
||||
const all_Ms = all.filter(sr => sr.studyrightid.slice(-2) !== '-2')
|
||||
const all_MsStudentnumbers = all_Ms.map(sr => sr.studentnumber)
|
||||
const all_BcMsStudentnumbers = all_BcMs.map(sr => sr.studentnumber)
|
||||
mastersTableStats[indexOf(reversedYears, year)][1] += all_Ms.length || 0
|
||||
bachelorMastersTableStats[indexOf(reversedYears, year)][1] += all_BcMs.length || 0
|
||||
|
||||
const msStudents = students.filter(student => all_MsStudentnumbers.includes(student.studentnumber))
|
||||
const bcMsStudents = students.filter(student => all_BcMsStudentnumbers.includes(student.studentnumber))
|
||||
|
||||
const data = getStudentData(
|
||||
startDate,
|
||||
bcMsStudents,
|
||||
creditThresholdKeys,
|
||||
creditThresholdAmounts,
|
||||
bcmslimits,
|
||||
limitKeys,
|
||||
'MH'
|
||||
)
|
||||
const bcMsStudentdata = data[0]
|
||||
const bcMsProgrammedata = data[1]
|
||||
const { msOnlyCreditThresholdKeys, msOnlyCreditThresholdAmount } = getOnlyMasterThresholds()
|
||||
const data2 = getStudentData(
|
||||
startDate,
|
||||
msStudents,
|
||||
msOnlyCreditThresholdKeys,
|
||||
msOnlyCreditThresholdAmount,
|
||||
masterlimits,
|
||||
limitKeys,
|
||||
'MH'
|
||||
)
|
||||
const msStudentdata = data2[0]
|
||||
const msProgrammedata = data2[1]
|
||||
if (!(progId in mastersProgrammeStats)) {
|
||||
mastersProgrammeStats[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
if (!(progId in bcMsProgrammeStats)) {
|
||||
bcMsProgrammeStats[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
if (year !== 'Total') {
|
||||
mastersProgrammeStats[progId][indexOf(reversedYears, year)] = limitKeys.map(key => msProgrammedata[key])
|
||||
bcMsProgrammeStats[progId][indexOf(reversedYears, year)] = limitKeys.map(key => bcMsProgrammedata[key])
|
||||
}
|
||||
for (const key of Object.keys(msStudentdata)) {
|
||||
mastersTableStats[indexOf(reversedYears, year)][indexOf(msOnlyCreditThresholdKeys, key) + 2] +=
|
||||
msStudentdata[key] || 0
|
||||
allMastersGraphStats[key].data[indexOf(yearsArray, year)] += msStudentdata[key] || 0
|
||||
}
|
||||
for (const key of Object.keys(bcMsStudentdata)) {
|
||||
bachelorMastersTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
bcMsStudentdata[key] || 0
|
||||
allBsMsGraphstats[key].data[indexOf(yearsArray, year)] += bcMsStudentdata[key] || 0
|
||||
}
|
||||
} else {
|
||||
const data = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts, [], [], 'T')
|
||||
const studentData = data[0]
|
||||
doctoralTableStats[indexOf(reversedYears, year)][1] += all.length || 0
|
||||
if (!(progId in doctoralProgrammeStats)) {
|
||||
doctoralProgrammeStats[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
if (year !== 'Total') {
|
||||
doctoralProgrammeStats[progId][indexOf(reversedYears, year)] = creditThresholdKeys.map(
|
||||
key => studentData[key]
|
||||
)
|
||||
}
|
||||
for (const key of Object.keys(studentData)) {
|
||||
doctoralTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
studentData[key] || 0
|
||||
allDoctoralGraphStats[key].data[indexOf(yearsArray, year)] += studentData[key] || 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const facultyProgressStats = {
|
||||
id: faculty,
|
||||
years: yearsArray,
|
||||
bachelorsTableStats: bachelorsTableStats,
|
||||
bcMsTableStats: bachelorMastersTableStats,
|
||||
mastersTableStats: mastersTableStats,
|
||||
doctoralTableStats: doctoralTableStats,
|
||||
bachelorsGraphStats: allBachelorGraphStats,
|
||||
bcMsGraphStats: allBsMsGraphstats,
|
||||
mastersGraphStats: allMastersGraphStats,
|
||||
doctoralGraphStats: allDoctoralGraphStats,
|
||||
bachelorTitles: tableTitles.creditProgress.bachelor,
|
||||
bcMsTitles: tableTitles.creditProgress.master,
|
||||
mastersTitles: tableTitles.creditProgress.masterOnly,
|
||||
doctoralTitles: tableTitles.creditProgress.doctoral,
|
||||
yearlyBachelorTitles: yearlyBachelorTitles,
|
||||
yearlyBcMsTitles: yearlyBcMsTitles,
|
||||
yearlyMasterTitles: yearlyMasterTitles,
|
||||
programmeNames: programmes.reduce(
|
||||
(obj, dataItem) => ({ ...obj, [dataItem.progId]: { code: dataItem.code, ...dataItem.name } }),
|
||||
{}
|
||||
),
|
||||
bachelorsProgrammeStats: bachelorsProgrammeStats,
|
||||
bcMsProgrammeStats: bcMsProgrammeStats,
|
||||
mastersProgrammeStats: mastersProgrammeStats,
|
||||
doctoralProgrammeStats: doctoralProgrammeStats,
|
||||
}
|
||||
return facultyProgressStats
|
||||
}
|
||||
module.exports = { combineFacultyStudentProgress }
|
||||
@@ -0,0 +1,160 @@
|
||||
const {
|
||||
getStartDate,
|
||||
getYearsArray,
|
||||
getPercentage,
|
||||
getYearsObject,
|
||||
getCorrectStudentnumbers,
|
||||
tableTitles,
|
||||
} = require('../studyprogrammeHelpers')
|
||||
const {
|
||||
studytrackStudents,
|
||||
allStudyrights,
|
||||
startedStudyrights,
|
||||
inactiveStudyrights,
|
||||
enrolledStudents,
|
||||
absentStudents,
|
||||
graduatedStudyRights,
|
||||
} = require('../studyprogramme')
|
||||
const { getAcademicYearDates } = require('../../util/semester')
|
||||
|
||||
const getStudentData = students => {
|
||||
let data = { female: 0, male: 0, finnish: 0 }
|
||||
|
||||
students.forEach(({ gender_code, home_country_en }) => {
|
||||
data.male += gender_code === '1' ? 1 : 0
|
||||
data.female += gender_code === '2' ? 1 : 0
|
||||
data.finnish += home_country_en === 'Finland' ? 1 : 0
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
// Goes through the faculty and its study programmes for the year
|
||||
const getFacultyDataForYear = async ({ programmes, since, settings, year, years, programmeTableStats }) => {
|
||||
const { includeAllSpecials, includeGraduated } = settings
|
||||
const { startDate, endDate } = getAcademicYearDates(year, since)
|
||||
// Totals for the year into table stats
|
||||
const totals = {
|
||||
total: 0,
|
||||
allStarted: 0,
|
||||
allEnrolled: 0,
|
||||
allAbsent: 0,
|
||||
allInactive: 0,
|
||||
allGraduated: 0,
|
||||
allFinnish: 0,
|
||||
allFemale: 0,
|
||||
allMale: 0,
|
||||
}
|
||||
|
||||
for (const { progId, code } of programmes) {
|
||||
const studentnumbers = await getCorrectStudentnumbers({
|
||||
codes: [code],
|
||||
startDate,
|
||||
endDate,
|
||||
includeAllSpecials,
|
||||
includeGraduated,
|
||||
})
|
||||
|
||||
// Get all the studyrights and students for the calculations
|
||||
let all = await allStudyrights(code, studentnumbers)
|
||||
const students = await studytrackStudents(studentnumbers)
|
||||
const studentData = getStudentData(students)
|
||||
const started = await startedStudyrights(code, startDate, studentnumbers)
|
||||
const enrolled = await enrolledStudents(code, startDate, studentnumbers)
|
||||
const absent = await absentStudents(code, studentnumbers)
|
||||
const inactive = await inactiveStudyrights(code, studentnumbers)
|
||||
const graduated = await graduatedStudyRights(code, startDate, studentnumbers)
|
||||
|
||||
// Count stats for the programme
|
||||
if (!(progId in programmeTableStats)) {
|
||||
programmeTableStats[progId] = years.reduce((resultObj, yearKey) => {
|
||||
return { ...resultObj, [yearKey]: [] }
|
||||
}, {})
|
||||
}
|
||||
|
||||
programmeTableStats[progId][year] = [
|
||||
all.length,
|
||||
started.length,
|
||||
getPercentage(started.length, all.length),
|
||||
enrolled.length,
|
||||
getPercentage(enrolled.length, all.length),
|
||||
absent.length,
|
||||
getPercentage(absent.length, all.length),
|
||||
inactive.length,
|
||||
getPercentage(inactive.length, all.length),
|
||||
graduated.length,
|
||||
getPercentage(graduated.length, all.length),
|
||||
studentData.male,
|
||||
getPercentage(studentData.male, all.length),
|
||||
studentData.female,
|
||||
getPercentage(studentData.female, all.length),
|
||||
studentData.finnish,
|
||||
getPercentage(studentData.finnish, all.length),
|
||||
]
|
||||
|
||||
totals.total += all.length
|
||||
totals.allStarted += started.length
|
||||
totals.allEnrolled += enrolled.length
|
||||
totals.allAbsent += absent.length
|
||||
totals.allInactive += inactive.length
|
||||
totals.allGraduated += graduated.length
|
||||
totals.allFemale += studentData.female
|
||||
totals.allMale += studentData.male
|
||||
totals.allFinnish += studentData.finnish
|
||||
}
|
||||
|
||||
return totals
|
||||
}
|
||||
|
||||
// Combines all the data for faculty students table
|
||||
const combineFacultyStudents = async (code, programmes, specialGroups, graduated) => {
|
||||
// Only academic years are considered
|
||||
const isAcademicYear = true
|
||||
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
|
||||
const includeGraduated = graduated === 'GRADUATED_INCLUDED'
|
||||
const includeYearsCombined = true
|
||||
const since = getStartDate(code, isAcademicYear)
|
||||
const years = getYearsArray(since.getFullYear(), isAcademicYear, includeYearsCombined)
|
||||
const settings = { includeAllSpecials, includeGraduated }
|
||||
let facultyTableStats = getYearsObject({ years, emptyArrays: true })
|
||||
let programmeTableStats = {}
|
||||
|
||||
for (const year of years.reverse()) {
|
||||
const queryParams = { programmes, since, settings, year, years, programmeTableStats }
|
||||
const totals = await getFacultyDataForYear(queryParams)
|
||||
facultyTableStats[year] = [
|
||||
year,
|
||||
totals.total,
|
||||
totals.allStarted,
|
||||
getPercentage(totals.allStarted, totals.total),
|
||||
totals.allEnrolled,
|
||||
getPercentage(totals.allEnrolled, totals.total),
|
||||
totals.allAbsent,
|
||||
getPercentage(totals.allAbsent, totals.total),
|
||||
totals.allInactive,
|
||||
getPercentage(totals.allInactive, totals.total),
|
||||
totals.allGraduated,
|
||||
getPercentage(totals.allGraduated, totals.total),
|
||||
totals.allMale,
|
||||
getPercentage(totals.allMale, totals.total),
|
||||
totals.allFemale,
|
||||
getPercentage(totals.allFemale, totals.total),
|
||||
totals.allFinnish,
|
||||
getPercentage(totals.allFinnish, totals.total),
|
||||
]
|
||||
}
|
||||
|
||||
const studentsData = {
|
||||
id: code,
|
||||
years: years,
|
||||
facultyTableStats: facultyTableStats,
|
||||
programmeStats: programmeTableStats,
|
||||
titles: tableTitles['studytracks'],
|
||||
programmeNames: programmes.reduce(
|
||||
(obj, dataItem) => ({ ...obj, [dataItem.progId]: { code: dataItem.code, ...dataItem.name } }),
|
||||
{}
|
||||
),
|
||||
}
|
||||
return studentsData
|
||||
}
|
||||
|
||||
module.exports = { combineFacultyStudents }
|
||||
@@ -0,0 +1,134 @@
|
||||
const { indexOf } = require('lodash')
|
||||
const { mapToProviders } = require('../../util/utils')
|
||||
const { thesisWriters } = require('./faculty')
|
||||
const {
|
||||
getStatsBasis,
|
||||
getYearsArray,
|
||||
defineYear,
|
||||
getCorrectStudentnumbers,
|
||||
alltimeEndDate,
|
||||
alltimeStartDate,
|
||||
} = require('../studyprogrammeHelpers')
|
||||
|
||||
const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyProgrammes, includeAllSpecials }) => {
|
||||
const thesisTypes = [
|
||||
'urn:code:course-unit-type:bachelors-thesis',
|
||||
'urn:code:course-unit-type:masters-thesis',
|
||||
'urn:code:course-unit-type:doctors-thesis',
|
||||
'urn:code:course-unit-type:licentiate-thesis',
|
||||
]
|
||||
|
||||
let bachelors = getStatsBasis(years)
|
||||
let masters = getStatsBasis(years)
|
||||
let doctors = getStatsBasis(years)
|
||||
let licentiates = getStatsBasis(years)
|
||||
let programmeCounts = {}
|
||||
let programmeNames = {}
|
||||
|
||||
for (const { progId, code, name } of facultyProgrammes) {
|
||||
const provider = mapToProviders([code])[0]
|
||||
const students = await getCorrectStudentnumbers({
|
||||
codes: [code],
|
||||
startDate: alltimeStartDate,
|
||||
endDate: alltimeEndDate,
|
||||
includeAllSpecials,
|
||||
})
|
||||
const thesisCourseCodes = await thesisWriters(provider, since, thesisTypes, students)
|
||||
thesisCourseCodes?.forEach(({ attainment_date, courseUnitType }) => {
|
||||
const thesisYear = defineYear(attainment_date, isAcademicYear)
|
||||
|
||||
if (!(progId in programmeCounts)) {
|
||||
programmeCounts[progId] = {}
|
||||
|
||||
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[progId][year] = [0, 0, 0, 0, 0]))
|
||||
programmeNames[progId] = { ...name, code: code }
|
||||
}
|
||||
programmeCounts[progId][thesisYear][0] += 1
|
||||
|
||||
if (courseUnitType === thesisTypes[0]) {
|
||||
bachelors.graphStats[indexOf(years, thesisYear)] += 1
|
||||
bachelors.tableStats[thesisYear] += 1
|
||||
programmeCounts[progId][thesisYear][1] += 1
|
||||
} else if (courseUnitType === thesisTypes[1]) {
|
||||
masters.graphStats[indexOf(years, thesisYear)] += 1
|
||||
masters.tableStats[thesisYear] += 1
|
||||
programmeCounts[progId][thesisYear][2] += 1
|
||||
} else if (courseUnitType === thesisTypes[2]) {
|
||||
doctors.graphStats[indexOf(years, thesisYear)] += 1
|
||||
doctors.tableStats[thesisYear] += 1
|
||||
programmeCounts[progId][thesisYear][3] += 1
|
||||
} else if (courseUnitType === thesisTypes[3]) {
|
||||
licentiates.graphStats[indexOf(years, thesisYear)] += 1
|
||||
licentiates.tableStats[thesisYear] += 1
|
||||
programmeCounts[progId][thesisYear][4] += 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { bachelors, masters, doctors, licentiates, programmeCounts, programmeNames }
|
||||
}
|
||||
|
||||
const getFacultyThesisWritersForStudyTrack = async (
|
||||
allThesisWriters,
|
||||
facultyProgrammes,
|
||||
isAcademicYear,
|
||||
includeAllSpecials
|
||||
) => {
|
||||
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
|
||||
const years = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
const queryParameters = { since, years, isAcademicYear, facultyProgrammes, includeAllSpecials }
|
||||
const { bachelors, masters, doctors, licentiates, programmeCounts, programmeNames } = await getFacultyThesisWriters(
|
||||
queryParameters
|
||||
)
|
||||
|
||||
allThesisWriters.years = years.map(year => year.toString())
|
||||
const reversedYears = years.reverse()
|
||||
allThesisWriters.tableStats = reversedYears.map(year => [
|
||||
year,
|
||||
bachelors.tableStats[year] + masters.tableStats[year] + doctors.tableStats[year] + licentiates.tableStats[year],
|
||||
bachelors.tableStats[year],
|
||||
masters.tableStats[year],
|
||||
doctors.tableStats[year],
|
||||
licentiates.tableStats[year],
|
||||
])
|
||||
allThesisWriters.graphStats.push({ name: 'Bachelors', data: bachelors.graphStats })
|
||||
allThesisWriters.graphStats.push({ name: 'Masters', data: masters.graphStats })
|
||||
allThesisWriters.graphStats.push({ name: 'Doctors', data: doctors.graphStats })
|
||||
allThesisWriters.graphStats.push({ name: 'Licentiates', data: licentiates.graphStats })
|
||||
|
||||
const programmes = programmeNames ? Object.keys(programmeNames) : facultyProgrammes.map(programme => programme.code)
|
||||
programmes.forEach(programmeId => {
|
||||
reversedYears.forEach(year => {
|
||||
if (programmeCounts && programmeId in programmeCounts) {
|
||||
if (!(programmeId in allThesisWriters.programmeTableStats)) {
|
||||
allThesisWriters.programmeTableStats[programmeId] = []
|
||||
}
|
||||
allThesisWriters.programmeTableStats[programmeId].push([year, ...programmeCounts[programmeId][year]])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
allThesisWriters.programmeNames = programmeNames
|
||||
}
|
||||
|
||||
const combineFacultyThesisWriters = async (faculty, facultyProgrammes, yearType, specialGroups) => {
|
||||
let allThesisWriters = {
|
||||
id: faculty,
|
||||
years: [],
|
||||
tableStats: [],
|
||||
graphStats: [],
|
||||
programmeTableStats: {},
|
||||
titles: ['', 'All', 'Bachelors', 'Masters', 'Doctors', 'Licentiates'],
|
||||
programmeNames: {},
|
||||
status: 'Done',
|
||||
lastUpdated: '',
|
||||
}
|
||||
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
|
||||
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
|
||||
|
||||
await getFacultyThesisWritersForStudyTrack(allThesisWriters, facultyProgrammes, isAcademicYear, includeAllSpecials)
|
||||
|
||||
return allThesisWriters
|
||||
}
|
||||
|
||||
module.exports = { combineFacultyThesisWriters }
|
||||
@@ -0,0 +1,188 @@
|
||||
const logger = require('../../util/logger')
|
||||
const { findFacultyProgrammeCodes } = require('./faculty')
|
||||
const { combineFacultyBasics } = require('./facultyBasics')
|
||||
const { combineFacultyCredits } = require('./facultyCredits')
|
||||
const { combineFacultyThesisWriters } = require('./facultyThesisWriters')
|
||||
const { countGraduationTimes } = require('./facultyGraduationTimes')
|
||||
const {
|
||||
setFacultyProgrammes,
|
||||
setBasicStats,
|
||||
setCreditStats,
|
||||
setThesisWritersStats,
|
||||
setGraduationStats,
|
||||
setFacultyProgressStats,
|
||||
setFacultyStudentStats,
|
||||
} = require('./facultyService')
|
||||
const { combineFacultyStudentProgress } = require('./facultyStudentProgress')
|
||||
const { combineFacultyStudents } = require('./facultyStudents')
|
||||
|
||||
const updateFacultyOverview = async (faculty, statsType) => {
|
||||
const calendarNewSpecial = {
|
||||
yearType: 'CALENDAR_YEAR',
|
||||
programmeFilter: 'NEW_STUDY_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_INCLUDED',
|
||||
}
|
||||
const calendarNewSpecialOut = {
|
||||
yearType: 'CALENDAR_YEAR',
|
||||
programmeFilter: 'NEW_STUDY_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_EXCLUDED',
|
||||
}
|
||||
|
||||
const calendarAllSpecial = {
|
||||
yearType: 'CALENDAR_YEAR',
|
||||
programmeFilter: 'ALL_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_INCLUDED',
|
||||
}
|
||||
const calendarAllSpecialOut = {
|
||||
yearType: 'CALENDAR_YEAR',
|
||||
programmeFilter: 'ALL_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_EXCLUDED',
|
||||
}
|
||||
const academicNewSpecial = {
|
||||
yearType: 'ACADEMIC_YEAR',
|
||||
programmeFilter: 'NEW_STUDY_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_INCLUDED',
|
||||
}
|
||||
|
||||
const academicAllSpecial = {
|
||||
yearType: 'ACADEMIC_YEAR',
|
||||
programmeFilter: 'ALL_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_INCLUDED',
|
||||
}
|
||||
|
||||
const academicNewSpecialOut = {
|
||||
yearType: 'ACADEMIC_YEAR',
|
||||
programmeFilter: 'NEW_STUDY_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_EXCLUDED',
|
||||
}
|
||||
|
||||
const academicAllSpecialOut = {
|
||||
yearType: 'ACADEMIC_YEAR',
|
||||
programmeFilter: 'ALL_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_EXCLUDED',
|
||||
}
|
||||
|
||||
const options = [
|
||||
calendarNewSpecial,
|
||||
calendarAllSpecial,
|
||||
academicNewSpecial,
|
||||
academicAllSpecial,
|
||||
calendarNewSpecialOut,
|
||||
calendarAllSpecialOut,
|
||||
academicNewSpecialOut,
|
||||
academicAllSpecialOut,
|
||||
]
|
||||
let allProgrammes = []
|
||||
let newProgrammes = []
|
||||
let allProgrammeCodes = []
|
||||
|
||||
try {
|
||||
const all = await findFacultyProgrammeCodes(faculty, 'ALL_PROGRAMMES')
|
||||
const onlyNew = await findFacultyProgrammeCodes(faculty, 'NEW_STUDY_PROGRAMMES')
|
||||
allProgrammes = await setFacultyProgrammes(faculty, all, 'ALL_PROGRAMMES')
|
||||
newProgrammes = await setFacultyProgrammes(faculty, onlyNew, 'NEW_STUDY_PROGRAMMES')
|
||||
allProgrammes?.data.forEach(prog => allProgrammeCodes.push(prog.code))
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
|
||||
for (const option of options) {
|
||||
const { yearType, programmeFilter, specialGroups } = option
|
||||
try {
|
||||
if (statsType === 'ALL' || statsType === 'STUDENT') {
|
||||
const updatedStudentInfo = await combineFacultyBasics(
|
||||
faculty,
|
||||
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
|
||||
yearType,
|
||||
allProgrammeCodes,
|
||||
programmeFilter,
|
||||
specialGroups
|
||||
)
|
||||
await setBasicStats(updatedStudentInfo, option.yearType, option.programmeFilter, option.specialGroups)
|
||||
}
|
||||
if (statsType === 'ALL' || statsType === 'CREDITS') {
|
||||
const updatedCredits = await combineFacultyCredits(
|
||||
faculty,
|
||||
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
|
||||
yearType,
|
||||
specialGroups
|
||||
)
|
||||
await setCreditStats(updatedCredits, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
if (statsType === 'ALL' || statsType === 'THESIS') {
|
||||
const updateThesisWriters = await combineFacultyThesisWriters(
|
||||
faculty,
|
||||
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
|
||||
yearType,
|
||||
specialGroups
|
||||
)
|
||||
await setThesisWritersStats(updateThesisWriters, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedTimesAll = await countGraduationTimes(faculty, 'ALL_PROGRAMMES')
|
||||
await setGraduationStats(updatedTimesAll, 'ALL_PROGRAMMES')
|
||||
const updatedTimesNew = await countGraduationTimes(faculty, 'NEW_STUDY_PROGRAMMES')
|
||||
await setGraduationStats(updatedTimesNew, 'NEW_STUDY_PROGRAMMES')
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
return 'OK'
|
||||
}
|
||||
|
||||
const updateFacultyProgressOverview = async faculty => {
|
||||
const specialGraduated = {
|
||||
specialGroups: 'SPECIAL_INCLUDED',
|
||||
graduated: 'GRADUATED_INCLUDED',
|
||||
}
|
||||
const specialNotGraduated = {
|
||||
specialGroups: 'SPECIAL_INCLUDED',
|
||||
graduated: 'GRADUATED_EXCLUDED',
|
||||
}
|
||||
const notSpecialGraduated = {
|
||||
specialGroups: 'SPECIAL_EXCLUDED',
|
||||
graduated: 'GRADUATED_INCLUDED',
|
||||
}
|
||||
const notSpecialNotGraduated = {
|
||||
specialGroups: 'SPECIAL_EXCLUDED',
|
||||
graduated: 'GRADUATED_EXCLUDED',
|
||||
}
|
||||
const options = [specialGraduated, specialNotGraduated, notSpecialGraduated, notSpecialNotGraduated]
|
||||
|
||||
let newProgrammes = []
|
||||
try {
|
||||
const onlyNew = await findFacultyProgrammeCodes(faculty, 'NEW_STUDY_PROGRAMMES')
|
||||
newProgrammes = await setFacultyProgrammes(faculty, onlyNew, 'NEW_STUDY_PROGRAMMES')
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
|
||||
for (const option of options) {
|
||||
const { specialGroups, graduated } = option
|
||||
try {
|
||||
const updateFacultyStudentStats = await combineFacultyStudents(
|
||||
faculty,
|
||||
newProgrammes.data,
|
||||
specialGroups,
|
||||
graduated
|
||||
)
|
||||
await setFacultyStudentStats(updateFacultyStudentStats, specialGroups, graduated)
|
||||
const updateFacultyProgressStats = await combineFacultyStudentProgress(
|
||||
faculty,
|
||||
newProgrammes.data,
|
||||
specialGroups,
|
||||
graduated
|
||||
)
|
||||
await setFacultyProgressStats(updateFacultyProgressStats, specialGroups, graduated)
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
}
|
||||
return 'OK'
|
||||
}
|
||||
|
||||
module.exports = { updateFacultyOverview, updateFacultyProgressOverview }
|
||||
@@ -1,9 +1,9 @@
|
||||
const axios = require('axios')
|
||||
const { isProduction, pateToken } = require('../conf-backend')
|
||||
const logger = require('../util/logger')
|
||||
const { ApplicationError } = require('../util/customErrors')
|
||||
|
||||
const pateClient = axios.create({
|
||||
baseURL: 'https://pate.toska.cs.helsinki.fi',
|
||||
baseURL: 'https://importer.cs.helsinki.fi/api/pate/',
|
||||
params: {
|
||||
token: pateToken,
|
||||
},
|
||||
@@ -20,33 +20,22 @@ const baseSettings = {
|
||||
}
|
||||
|
||||
const sendEmail = async (options = {}) => {
|
||||
if (!isProduction) {
|
||||
const errorMessage = 'Skipped sending email in non-production environment'
|
||||
logger.error({ message: errorMessage, meta: options })
|
||||
return { error: errorMessage }
|
||||
}
|
||||
|
||||
if (!pateToken) {
|
||||
const errorMessage = 'Skipped sending email since pate token was not available'
|
||||
logger.error({ message: errorMessage, meta: options })
|
||||
return { error: errorMessage }
|
||||
}
|
||||
|
||||
const { data } = await pateClient.post('/', options)
|
||||
|
||||
return data
|
||||
if (!isProduction) throw new ApplicationError('Email sending is disabled in development mode.')
|
||||
if (!pateToken) throw new ApplicationError('Email sending failed because pate token is missing.')
|
||||
return await pateClient.post('/', options)
|
||||
}
|
||||
|
||||
const sendFeedbackToToska = ({ feedbackContent, userId, userEmail, userFullName }) => {
|
||||
const userDetails = `Sent by ${userFullName}, userid: ${userId}, email: ${userEmail}`
|
||||
const sendFeedbackToToska = async ({ feedbackContent, user }) => {
|
||||
const { name, userId, email } = user
|
||||
const userDetails = `Sent by ${name}, userid: ${userId}, email: ${email}`
|
||||
const text = [feedbackContent, userDetails].join('<br />')
|
||||
return sendEmail({
|
||||
await sendEmail({
|
||||
template,
|
||||
emails: [
|
||||
{
|
||||
to: 'Toska <grp-toska@helsinki.fi>',
|
||||
replyTo: userEmail,
|
||||
subject: `Oodikone feedback from ${userFullName}`,
|
||||
replyTo: email,
|
||||
subject: `Oodikone feedback from ${name}`,
|
||||
text,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,338 +0,0 @@
|
||||
const sequelize = require('sequelize')
|
||||
const { Op } = sequelize
|
||||
const {
|
||||
Credit,
|
||||
Course,
|
||||
Organization,
|
||||
Studyright,
|
||||
StudyrightElement,
|
||||
ElementDetail,
|
||||
Transfer,
|
||||
Student,
|
||||
SemesterEnrollment,
|
||||
Semester,
|
||||
} = require('../models')
|
||||
const { formatStudyright, formatStudent } = require('./studyprogrammeHelpers')
|
||||
|
||||
const whereStudents = studentnumbers => {
|
||||
return studentnumbers ? studentnumbers : { [Op.not]: null }
|
||||
}
|
||||
|
||||
const studytrackStudents = async studentnumbers =>
|
||||
(
|
||||
await Student.findAll({
|
||||
where: {
|
||||
studentnumber: {
|
||||
[Op.in]: studentnumbers,
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(formatStudent)
|
||||
|
||||
const enrolledStudyrights = async studytrack => {
|
||||
const studyrights = await Studyright.findAll({
|
||||
attributes: ['studyrightid', 'studystartdate', 'enddate', 'prioritycode'],
|
||||
include: [
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
where: { code: studytrack },
|
||||
},
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const studentnumbers = studyrights.map(s => s.student.studentnumber)
|
||||
|
||||
const students = await Student.findAll({
|
||||
attributes: ['studentnumber'],
|
||||
include: [
|
||||
{
|
||||
model: Studyright,
|
||||
include: [
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
where: {
|
||||
code: studytrack,
|
||||
},
|
||||
},
|
||||
],
|
||||
attributes: ['studyrightid'],
|
||||
},
|
||||
{
|
||||
model: SemesterEnrollment,
|
||||
attributes: ['semestercode'],
|
||||
include: [
|
||||
{
|
||||
model: Semester,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
where: {
|
||||
enrollmenttype: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
studentnumber: {
|
||||
[Op.in]: studentnumbers,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
let enrollments = new Set()
|
||||
|
||||
students.forEach(student => {
|
||||
student.semester_enrollments.forEach(e => {
|
||||
const enrollmentStartDate = e.semester.startdate
|
||||
const enrollmentEndDate = e.semester.enddate
|
||||
const correctStudyRight = studyrights.find(
|
||||
s =>
|
||||
student.studentnumber === s.student.studentnumber &&
|
||||
s.studystartdate <= enrollmentStartDate && // Studying in the programme should have started on the same day or earlier than the enrollment
|
||||
s.enddate >= enrollmentEndDate && // Studying in the programme should have ended on the same day or later than the enrollment
|
||||
(!s.canceldate || s.canceldate > enrollmentStartDate) && // Studyright should not be canceled OR the possible canceldate should be after the enrollmentStartDate
|
||||
(s.prioritycode === 1 || s.prioritycode === 30) // Studyright should be primary or the student should have graduated
|
||||
)
|
||||
if (correctStudyRight) {
|
||||
enrollments.add({
|
||||
studentnumber: student.studentnumber,
|
||||
startdate: e.semester.startdate,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return enrollments
|
||||
}
|
||||
|
||||
const startedStudyrights = async (studytrack, since, studentnumbers) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
required: true,
|
||||
where: {
|
||||
code: studytrack,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
where: {
|
||||
studystartdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
student_studentnumber: whereStudents(studentnumbers),
|
||||
},
|
||||
})
|
||||
).map(formatStudyright)
|
||||
|
||||
const graduatedStudyRights = async (studytrack, since, studentnumbers) =>
|
||||
await Studyright.findAll({
|
||||
include: {
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
required: true,
|
||||
where: {
|
||||
code: studytrack,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
graduated: 1,
|
||||
enddate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
student_studentnumber: whereStudents(studentnumbers),
|
||||
},
|
||||
})
|
||||
|
||||
const cancelledStudyRights = async (studytrack, since) => {
|
||||
return await Studyright.findAll({
|
||||
include: {
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
where: {
|
||||
code: {
|
||||
[Op.eq]: studytrack,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
canceldate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const transfersAway = async (studytrack, since) => {
|
||||
return await Transfer.findAll({
|
||||
where: {
|
||||
transferdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
sourcecode: studytrack,
|
||||
},
|
||||
distinct: true,
|
||||
col: 'studentnumber',
|
||||
})
|
||||
}
|
||||
|
||||
const transfersTo = async (studytrack, since) => {
|
||||
return await Transfer.findAll({
|
||||
where: {
|
||||
transferdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
targetcode: studytrack,
|
||||
},
|
||||
distinct: true,
|
||||
col: 'studentnumber',
|
||||
})
|
||||
}
|
||||
|
||||
const getProgrammesStudents = async studyprogramme =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
attributes: ['studyrightid', 'studystartdate', 'enddate', 'graduated', 'prioritycode', 'extentcode'],
|
||||
include: [
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: [studyprogramme],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
).map(formatStudyright)
|
||||
|
||||
const getCreditsForStudyProgramme = async (provider, since) =>
|
||||
await Credit.findAll({
|
||||
attributes: ['id', 'course_code', 'credits', 'attainment_date', 'student_studentnumber'],
|
||||
include: {
|
||||
model: Course,
|
||||
attributes: ['code'],
|
||||
required: true,
|
||||
where: {
|
||||
is_study_module: false,
|
||||
},
|
||||
include: {
|
||||
model: Organization,
|
||||
required: true,
|
||||
where: {
|
||||
code: provider,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
credittypecode: {
|
||||
[Op.notIn]: [10, 9, 7],
|
||||
},
|
||||
isStudyModule: {
|
||||
[Op.not]: true,
|
||||
},
|
||||
attainment_date: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const getTransferredCredits = async (provider, since) =>
|
||||
await Credit.findAll({
|
||||
attributes: ['id', 'course_code', 'credits', 'attainment_date', 'credittypecode', 'student_studentnumber'],
|
||||
include: {
|
||||
model: Course,
|
||||
attributes: ['code'],
|
||||
required: true,
|
||||
where: {
|
||||
is_study_module: false,
|
||||
},
|
||||
include: {
|
||||
model: Organization,
|
||||
required: true,
|
||||
where: {
|
||||
code: provider,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
credittypecode: {
|
||||
[Op.eq]: [9],
|
||||
},
|
||||
attainment_date: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const getThesisCredits = async (provider, since, thesisType) =>
|
||||
await Credit.findAll({
|
||||
attributes: ['id', 'course_code', 'credits', 'attainment_date', 'student_studentnumber'],
|
||||
include: {
|
||||
model: Course,
|
||||
attributes: ['code'],
|
||||
required: true,
|
||||
where: {
|
||||
course_unit_type: {
|
||||
[Op.in]: thesisType,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
model: Organization,
|
||||
required: true,
|
||||
where: {
|
||||
code: provider,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
credittypecode: {
|
||||
[Op.notIn]: [10, 9, 7],
|
||||
},
|
||||
isStudyModule: {
|
||||
[Op.not]: true,
|
||||
},
|
||||
attainment_date: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
studytrackStudents,
|
||||
enrolledStudyrights,
|
||||
startedStudyrights,
|
||||
graduatedStudyRights,
|
||||
cancelledStudyRights,
|
||||
transfersAway,
|
||||
transfersTo,
|
||||
getProgrammesStudents,
|
||||
getCreditsForStudyProgramme,
|
||||
getTransferredCredits,
|
||||
getThesisCredits,
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
const mapOpenCredits = credit => {
|
||||
const { course_code, attainment_date, student_studentnumber } = credit
|
||||
return {
|
||||
course_code,
|
||||
attainment_date,
|
||||
student_studentnumber,
|
||||
}
|
||||
}
|
||||
|
||||
const mapStundentInfo = student => {
|
||||
const { email, secondary_email, studentnumber, dissemination_info_allowed } = student
|
||||
return {
|
||||
studentnumber,
|
||||
email,
|
||||
secondary_email,
|
||||
dissemination_info_allowed,
|
||||
}
|
||||
}
|
||||
const mapOpenEnrollments = enrollment => {
|
||||
const { studentnumber, course_code, state, enrollment_date_time } = enrollment
|
||||
return {
|
||||
enrollmentStudentnumber: studentnumber,
|
||||
course_code,
|
||||
state,
|
||||
enrollment_date_time,
|
||||
}
|
||||
}
|
||||
|
||||
const mapStudyRights = studyright => {
|
||||
const { enddate, startdate, student } = studyright
|
||||
|
||||
return {
|
||||
startdate,
|
||||
enddate,
|
||||
studyrightStudentnumber: student.studentnumber,
|
||||
}
|
||||
}
|
||||
|
||||
const mapCourseInfo = course => {
|
||||
const { code, name } = course
|
||||
return {
|
||||
label: code,
|
||||
name,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { mapOpenCredits, mapOpenEnrollments, mapStundentInfo, mapStudyRights, mapCourseInfo }
|
||||
@@ -0,0 +1,65 @@
|
||||
const logger = require('../../util/logger')
|
||||
const {
|
||||
getOpenUniSearchesByUser,
|
||||
createOpenUniPopulationSearch,
|
||||
updateOpenUniPopulationSearch,
|
||||
deleteOpenUniSearch,
|
||||
} = require('./openUniSearches')
|
||||
|
||||
const getOpenUniSearches = async userId => {
|
||||
const savedSearches = await getOpenUniSearchesByUser(userId)
|
||||
if (savedSearches === undefined) return []
|
||||
const foundSearches = savedSearches.reduce(
|
||||
(res, search) => [
|
||||
...res,
|
||||
{
|
||||
id: search.id,
|
||||
userId: search.userId,
|
||||
name: search.name,
|
||||
courseList: search.courseCodes,
|
||||
updatedAt: search.updatedAt,
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
return foundSearches
|
||||
}
|
||||
|
||||
const createNewSearch = async (userId, name, courseCodes) => {
|
||||
const savedSearches = await getOpenUniSearchesByUser(userId)
|
||||
if (savedSearches !== undefined) {
|
||||
const searchNames = savedSearches.map(search => search.name)
|
||||
if (searchNames.includes(name)) return null
|
||||
}
|
||||
try {
|
||||
const created = await createOpenUniPopulationSearch(userId, name, courseCodes)
|
||||
if (!created) return null
|
||||
return created
|
||||
} catch (e) {
|
||||
logger.error(`Couldn't create new open uni search instance: ${e}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const deleteSearch = async (userId, id) => {
|
||||
try {
|
||||
const deleted = await deleteOpenUniSearch(userId, id)
|
||||
if (!deleted) return null
|
||||
return deleted
|
||||
} catch (e) {
|
||||
logger.error(`Couldn't delete open uni search instance: ${e}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const updateSearch = async (userId, id, courseCodes) => {
|
||||
try {
|
||||
const updated = await updateOpenUniPopulationSearch(userId, id, courseCodes)
|
||||
if (!updated) return null
|
||||
return updated
|
||||
} catch (e) {
|
||||
logger.error(`Couldn't update open uni search instance: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getOpenUniSearches, createNewSearch, deleteSearch, updateSearch }
|
||||
@@ -0,0 +1,137 @@
|
||||
const { Credit, Enrollment, Studyright, Student, Course } = require('../../models')
|
||||
const { OpenUniPopulationSearch } = require('../../models/models_kone')
|
||||
const {
|
||||
mapOpenCredits,
|
||||
mapOpenEnrollments,
|
||||
mapStundentInfo,
|
||||
mapStudyRights,
|
||||
mapCourseInfo,
|
||||
} = require('./openUniHelpers')
|
||||
const { Op } = require('sequelize')
|
||||
|
||||
// 1. iteration: time is hardcoded. Check hyväksytty grades
|
||||
const getCredits = async (courseCodes, startdate) =>
|
||||
(
|
||||
await Credit.findAll({
|
||||
where: {
|
||||
course_code: {
|
||||
[Op.in]: courseCodes,
|
||||
},
|
||||
is_open: true,
|
||||
grade: {
|
||||
[Op.in]: ['1', '2', '3', '4', '5', 'Hyv.', 'hyv', 'HT', 'TT'],
|
||||
},
|
||||
attainment_date: {
|
||||
[Op.gte]: startdate,
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(mapOpenCredits)
|
||||
|
||||
const getStudentInfo = async students =>
|
||||
(
|
||||
await Student.findAll({
|
||||
where: {
|
||||
studentnumber: {
|
||||
[Op.in]: students,
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(mapStundentInfo)
|
||||
|
||||
const getEnrollments = async (courseCodes, startdate, enddate) =>
|
||||
(
|
||||
await Enrollment.findAll({
|
||||
where: {
|
||||
course_code: {
|
||||
[Op.in]: courseCodes,
|
||||
},
|
||||
is_open: true,
|
||||
enrollment_date_time: {
|
||||
[Op.and]: {
|
||||
[Op.lte]: enddate,
|
||||
[Op.gte]: startdate,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(mapOpenEnrollments)
|
||||
|
||||
const getCourseNames = async courseCodes =>
|
||||
(
|
||||
await Course.findAll({
|
||||
attributes: ['code', 'name'],
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: courseCodes,
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(mapCourseInfo)
|
||||
|
||||
const getStudyRights = async students =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
where: {
|
||||
student_studentnumber: {
|
||||
[Op.in]: students.length > 0 ? students : { [Op.not]: null },
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(mapStudyRights)
|
||||
|
||||
const getOpenUniSearchesByUser = async userId => {
|
||||
return await OpenUniPopulationSearch.findAll({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const createOpenUniPopulationSearch = async (userId, name, courseCodes) => {
|
||||
return await OpenUniPopulationSearch.create({
|
||||
userId,
|
||||
name,
|
||||
courseCodes,
|
||||
})
|
||||
}
|
||||
|
||||
const updateOpenUniPopulationSearch = async (userId, id, courseCodes) => {
|
||||
const searchToUpdate = await OpenUniPopulationSearch.findOne({
|
||||
where: {
|
||||
userId: userId,
|
||||
id: id,
|
||||
},
|
||||
})
|
||||
|
||||
if (!searchToUpdate) return null
|
||||
return await searchToUpdate.update({ courseCodes })
|
||||
}
|
||||
|
||||
const deleteOpenUniSearch = async (userId, id) => {
|
||||
return await OpenUniPopulationSearch.destroy({
|
||||
where: {
|
||||
userId: userId,
|
||||
id: id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCredits,
|
||||
getEnrollments,
|
||||
getStudyRights,
|
||||
getStudentInfo,
|
||||
getCourseNames,
|
||||
getOpenUniSearchesByUser,
|
||||
createOpenUniPopulationSearch,
|
||||
updateOpenUniPopulationSearch,
|
||||
deleteOpenUniSearch,
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
const moment = require('moment')
|
||||
const { getCredits, getStudyRights, getEnrollments, getStudentInfo, getCourseNames } = require('./openUniSearches')
|
||||
|
||||
const uniq = objects => [...new Set(objects)]
|
||||
|
||||
const getCustomOpenUniCourses = async (courseCodes, startdate, enddate) => {
|
||||
const ayCourseCodes = courseCodes.map(courseCode => 'AY' + courseCode)
|
||||
const allCourseCodes = courseCodes.concat(ayCourseCodes)
|
||||
const allCredits = await getCredits(allCourseCodes, startdate)
|
||||
const allEnrollments = await getEnrollments(allCourseCodes, startdate, enddate)
|
||||
const students = uniq(allEnrollments.map(enrollment => enrollment.enrollmentStudentnumber))
|
||||
const courses = await getCourseNames(courseCodes)
|
||||
|
||||
const allStudyrights = await getStudyRights(students)
|
||||
const studentInfo = await getStudentInfo(students)
|
||||
// Filter out current studyrights:
|
||||
// Case 1: Both startdate and enddate are outside of the given interval
|
||||
// Case 2: Startdate is inside of the given interval and enddate is outside
|
||||
// Case 3: Startdate is before the interval start and the enddate is within the interval
|
||||
const studentsWithCurrentStudyRight = allStudyrights
|
||||
.filter(
|
||||
right =>
|
||||
moment(right.startdate).isBetween(startdate, moment()) ||
|
||||
(moment(right.startdate).isSameOrBefore(startdate) && moment(right.enddate).isSameOrAfter(moment())) ||
|
||||
moment(right.enddate).isSameOrBefore(moment())
|
||||
)
|
||||
.map(right => right.studyrightStudentnumber)
|
||||
const uniqueStudentsWithCurrentStudyRight = uniq(studentsWithCurrentStudyRight)
|
||||
|
||||
const studentStats = {}
|
||||
const getEmptyCourseInfo = () => {
|
||||
return {
|
||||
enrolledPassed: null,
|
||||
enrolledNotPassed: [],
|
||||
}
|
||||
}
|
||||
|
||||
for (const { studentnumber, email, secondary_email, dissemination_info_allowed } of studentInfo) {
|
||||
// Check if the student has existing studyright: if yes, then stop here
|
||||
if (!uniqueStudentsWithCurrentStudyRight.includes(studentnumber)) {
|
||||
if (!(studentnumber in studentStats)) {
|
||||
studentStats[studentnumber] = {
|
||||
courseInfo: courseCodes.reduce((acc, code) => ({ ...acc, [code]: getEmptyCourseInfo() }), {}),
|
||||
email: email,
|
||||
secondaryEmail: secondary_email,
|
||||
disseminationInfoAllowed: dissemination_info_allowed,
|
||||
passedTotal: 0,
|
||||
enrolledTotal: 0,
|
||||
}
|
||||
}
|
||||
const passedCourses = []
|
||||
const unfinishedCourses = []
|
||||
for (const { course_code, attainment_date, student_studentnumber } of allCredits) {
|
||||
if (student_studentnumber === studentnumber) {
|
||||
const courseCode = course_code.replace('AY', '')
|
||||
studentStats[studentnumber].courseInfo[courseCode].enrolledPassed = attainment_date
|
||||
if (!passedCourses.includes(courseCode)) {
|
||||
passedCourses.push(courseCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const { enrollmentStudentnumber, course_code, enrollment_date_time } of allEnrollments) {
|
||||
if (enrollmentStudentnumber === studentnumber) {
|
||||
const courseCode = course_code.replace('AY', '')
|
||||
if (!studentStats[studentnumber].courseInfo[courseCode].enrolledPassed && studentnumber === studentnumber) {
|
||||
studentStats[studentnumber].courseInfo[courseCode].enrolledNotPassed.push(enrollment_date_time)
|
||||
if (!unfinishedCourses.includes(courseCode)) unfinishedCourses.push(courseCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
studentStats[studentnumber].passedTotal = passedCourses.length
|
||||
studentStats[studentnumber].enrolledTotal = unfinishedCourses.length
|
||||
}
|
||||
}
|
||||
const openUniStats = { students: studentStats, courses: courses }
|
||||
return openUniStats
|
||||
}
|
||||
|
||||
module.exports = { getCustomOpenUniCourses }
|
||||
@@ -65,4 +65,4 @@ const parseCredit = (credit, anonymizationSalt) => {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { parseCredit }
|
||||
module.exports = { parseCredit, formatStudyrightElement }
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
const crypto = require('crypto')
|
||||
const { formatStudyrightElement } = require('./parseCredits')
|
||||
|
||||
const parseEnrollment = (enrollment, anonymizationSalt) => {
|
||||
const { student, semester, state, enrollment_date_time, course_code } = enrollment
|
||||
const { studentnumber, studyright_elements } = student
|
||||
const { yearcode, yearname, semestercode, name: semestername } = semester
|
||||
|
||||
if (anonymizationSalt) {
|
||||
const anonymizedStudentNumber = crypto
|
||||
.createHash('sha256')
|
||||
.update(`${studentnumber}${anonymizationSalt}`)
|
||||
.digest('hex')
|
||||
|
||||
const anonymizedStudent = {
|
||||
studentnumber: anonymizedStudentNumber,
|
||||
}
|
||||
|
||||
return {
|
||||
obfuscated: true,
|
||||
student: anonymizedStudent,
|
||||
yearcode,
|
||||
yearname,
|
||||
semestercode,
|
||||
semestername,
|
||||
coursecode: course_code,
|
||||
state,
|
||||
enrollment_date_time,
|
||||
studentnumber: anonymizedStudentNumber,
|
||||
programmes: studyright_elements.map(formatStudyrightElement),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
student,
|
||||
yearcode,
|
||||
yearname,
|
||||
semestercode,
|
||||
semestername,
|
||||
coursecode: course_code,
|
||||
state,
|
||||
enrollment_date_time,
|
||||
studentnumber,
|
||||
programmes: studyright_elements.map(formatStudyrightElement),
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { parseEnrollment }
|
||||
@@ -1,7 +1,7 @@
|
||||
const { Op } = require('sequelize')
|
||||
const Sequelize = require('sequelize')
|
||||
const { Op } = Sequelize
|
||||
const moment = require('moment')
|
||||
const { sortBy } = require('lodash')
|
||||
const { sortMainCode } = require('../util/utils')
|
||||
const { sortBy, keyBy } = require('lodash')
|
||||
|
||||
const {
|
||||
Student,
|
||||
@@ -10,11 +10,12 @@ const {
|
||||
Studyright,
|
||||
StudyrightExtent,
|
||||
ElementDetail,
|
||||
CourseType,
|
||||
Studyplan,
|
||||
SemesterEnrollment,
|
||||
Semester,
|
||||
Transfer,
|
||||
StudyrightElement,
|
||||
Enrollment,
|
||||
} = require('../models')
|
||||
const {
|
||||
dbConnections: { sequelize },
|
||||
@@ -23,6 +24,7 @@ const { Tag, TagStudent } = require('../models/models_kone')
|
||||
const { CourseStatsCounter } = require('./course_stats_counter')
|
||||
const { getPassingSemester, semesterEnd, semesterStart } = require('../util/semester')
|
||||
const { getAllProgrammes } = require('./studyrights')
|
||||
const { encrypt } = require('../services/encrypt')
|
||||
|
||||
const enrolmentDates = () => {
|
||||
const query = 'SELECT DISTINCT s.dateOfUniversityEnrollment as date FROM Student s'
|
||||
@@ -47,6 +49,7 @@ const formatStudentForPopulationStatistics = (
|
||||
abbreviatedname,
|
||||
email,
|
||||
studyrights,
|
||||
studyplans,
|
||||
semester_enrollments,
|
||||
transfers,
|
||||
updatedAt,
|
||||
@@ -58,7 +61,9 @@ const formatStudentForPopulationStatistics = (
|
||||
tags,
|
||||
option,
|
||||
birthdate,
|
||||
sis_person_id,
|
||||
},
|
||||
enrollments,
|
||||
credits,
|
||||
startDate,
|
||||
endDate,
|
||||
@@ -93,6 +98,7 @@ const formatStudentForPopulationStatistics = (
|
||||
studentNumber: studentnumber,
|
||||
credits: creditcount || 0,
|
||||
courses: credits[studentnumber] ? credits[studentnumber].map(toCourse) : [],
|
||||
enrollments: enrollments[studentnumber],
|
||||
name: abbreviatedname,
|
||||
transfers: transfers || [],
|
||||
gender_code,
|
||||
@@ -105,6 +111,8 @@ const formatStudentForPopulationStatistics = (
|
||||
starting: moment(started).isBetween(startDateMoment, endDateMoment, null, '[]'),
|
||||
option,
|
||||
birthdate,
|
||||
studyplans,
|
||||
sis_person_id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,10 +148,24 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
|
||||
if (tag) studentnumbers = studentnumbersWithTag
|
||||
|
||||
const attainmentDateFrom = tag ? moment(startDate).year(tag.year) : startDate
|
||||
const studyPlans = await Studyplan.findAll({
|
||||
where: { studentnumber: studentnumbers },
|
||||
attributes: ['included_courses'],
|
||||
raw: true,
|
||||
})
|
||||
const studyPlanCourses = Array.from(new Set([...studyPlans.map(plan => plan.included_courses)].flat()))
|
||||
|
||||
const creditsOfStudentOther = {
|
||||
attainment_date: {
|
||||
[Op.between]: [attainmentDateFrom, endDate],
|
||||
},
|
||||
[Op.or]: [
|
||||
{
|
||||
attainment_date: {
|
||||
[Op.between]: [attainmentDateFrom, endDate],
|
||||
},
|
||||
},
|
||||
{
|
||||
course_code: studyPlanCourses,
|
||||
},
|
||||
],
|
||||
student_studentnumber: {
|
||||
[Op.in]: studentnumbers,
|
||||
},
|
||||
@@ -162,7 +184,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
|
||||
},
|
||||
{
|
||||
course_code: {
|
||||
[Op.in]: ['375063', '339101'],
|
||||
[Op.in]: ['375063', '339101'].concat(studyPlanCourses),
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -173,8 +195,9 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
|
||||
: creditsOfStudentOther
|
||||
|
||||
if (studentnumbers.length === 0)
|
||||
return { students: [], credits: [], extents: [], semesters: [], elementdetails: [], courses: [] }
|
||||
const [courses, students, credits, extents, semesters, elementdetails] = await Promise.all([
|
||||
return { students: [], enrollments: [], credits: [], extents: [], semesters: [], elementdetails: [], courses: [] }
|
||||
|
||||
const [courses, enrollments, students, credits, extents, semesters, elementdetails] = await Promise.all([
|
||||
Course.findAll({
|
||||
attributes: [sequelize.literal('DISTINCT ON("code") code'), 'name', 'coursetypecode'],
|
||||
include: [
|
||||
@@ -186,6 +209,19 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
|
||||
],
|
||||
raw: true,
|
||||
}),
|
||||
Enrollment.findAll({
|
||||
attributes: ['course_code', 'state', 'enrollment_date_time', 'studentnumber', 'semestercode'],
|
||||
where: {
|
||||
enrollment_date_time: {
|
||||
[Op.between]: [attainmentDateFrom, endDate],
|
||||
},
|
||||
studentnumber: {
|
||||
[Op.in]: studentnumbers,
|
||||
},
|
||||
state: ['ENROLLED', 'CONFIRMED'],
|
||||
},
|
||||
raw: true,
|
||||
}),
|
||||
Student.findAll({
|
||||
attributes: [
|
||||
'firstnames',
|
||||
@@ -199,6 +235,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
|
||||
'updatedAt',
|
||||
'gender_code',
|
||||
'birthdate',
|
||||
'sis_person_id',
|
||||
/* 'gender_fi',
|
||||
'gender_sv',
|
||||
'gender_en' */
|
||||
@@ -217,7 +254,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
|
||||
'enddate',
|
||||
'extentcode',
|
||||
'graduated',
|
||||
'canceldate',
|
||||
'active',
|
||||
'prioritycode',
|
||||
'faculty_code',
|
||||
'studystartdate',
|
||||
@@ -250,6 +287,11 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
model: Studyplan,
|
||||
attributes: ['included_courses', 'programme_code', 'completed_credits'],
|
||||
separate: true,
|
||||
},
|
||||
],
|
||||
where: {
|
||||
studentnumber: {
|
||||
@@ -330,7 +372,7 @@ EXISTS (SELECT 1 FROM studyright_elements WHERE studentnumber IN (:studentnumber
|
||||
student.tags = studentNumberToTags[student.studentnumber] || []
|
||||
})
|
||||
|
||||
return { students, credits, extents, semesters, elementdetails, courses }
|
||||
return { students, enrollments, credits, extents, semesters, elementdetails, courses }
|
||||
}
|
||||
|
||||
const count = (column, count, distinct = false) => {
|
||||
@@ -361,17 +403,18 @@ const studentnumbersWithAllStudyrightElements = async (
|
||||
startDate,
|
||||
endDate,
|
||||
exchangeStudents,
|
||||
cancelledStudents,
|
||||
nondegreeStudents,
|
||||
transferredStudents,
|
||||
tag
|
||||
transferredOutStudents,
|
||||
tag,
|
||||
transferredToStudents,
|
||||
graduatedStudents
|
||||
) => {
|
||||
// eslint-disable-line
|
||||
|
||||
// db startdate is formatted to utc so need to change it when querying
|
||||
const formattedStartDate = new Date(moment.tz(startDate, 'Europe/Helsinki').format()).toUTCString()
|
||||
|
||||
const filteredExtents = [10] // always filter out secondary subject students
|
||||
const filteredExtents = [16] // always filter out secondary subject students
|
||||
let studyrightWhere = {
|
||||
extentcode: {
|
||||
[Op.notIn]: filteredExtents,
|
||||
@@ -381,10 +424,7 @@ const studentnumbersWithAllStudyrightElements = async (
|
||||
filteredExtents.push(7, 34)
|
||||
}
|
||||
if (!nondegreeStudents) {
|
||||
filteredExtents.push(33, 99, 14, 13)
|
||||
}
|
||||
if (!cancelledStudents) {
|
||||
studyrightWhere.canceldate = null
|
||||
filteredExtents.push(22, 99, 14, 13)
|
||||
}
|
||||
|
||||
let studentWhere = {}
|
||||
@@ -403,7 +443,7 @@ const studentnumbersWithAllStudyrightElements = async (
|
||||
}
|
||||
|
||||
const students = await Studyright.findAll({
|
||||
attributes: ['student_studentnumber', 'graduated'],
|
||||
attributes: ['student_studentnumber', 'graduated', 'enddate'],
|
||||
include: {
|
||||
model: StudyrightElement,
|
||||
attributes: [],
|
||||
@@ -444,7 +484,7 @@ const studentnumbersWithAllStudyrightElements = async (
|
||||
raw: true,
|
||||
})
|
||||
|
||||
const studentnumbers = [...new Set(students.map(s => s.student_studentnumber))]
|
||||
let studentnumbers = [...new Set(students.map(s => s.student_studentnumber))]
|
||||
|
||||
// bit hacky solution, but this is used to filter out studentnumbers who have since changed studytracks
|
||||
const rights = await Studyright.findAll({
|
||||
@@ -491,17 +531,21 @@ const studentnumbersWithAllStudyrightElements = async (
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
// Take the newest studytrack primarily by latest starting date in the track, secondarily by the latest enddate
|
||||
const filteredStudentnumbers = studentnumbers.filter(studentnumber => {
|
||||
const newestStudytrack = sortBy(formattedStudytracks[studentnumber], ['enddate', 'startdate']).reverse()[0]
|
||||
const newestStudytrack = sortBy(formattedStudytracks[studentnumber], ['startdate', 'enddate']).reverse()[0]
|
||||
if (!newestStudytrack) return false
|
||||
return studyRights.includes(newestStudytrack.code)
|
||||
})
|
||||
|
||||
const studentnumberlist = filteredStudentnumbers.length > 0 ? filteredStudentnumbers : studentnumbers
|
||||
// Use the filtered list, if the search includes studytracks
|
||||
// Then the studyrights length is > 1, which means that there is [studyright, studytrack].
|
||||
// When searching only for studyprogramme, there is [studyright]
|
||||
let studentnumberlist = studyRights.length > 1 ? filteredStudentnumbers : studentnumbers
|
||||
|
||||
// fetch students that have transferred out of the programme and filter out these studentnumbers
|
||||
if (!transferredStudents) {
|
||||
const transferredOutStudents = (
|
||||
if (!transferredOutStudents) {
|
||||
const transfersOut = (
|
||||
await Transfer.findAll({
|
||||
attributes: ['studentnumber'],
|
||||
where: {
|
||||
@@ -519,8 +563,63 @@ const studentnumbersWithAllStudyrightElements = async (
|
||||
})
|
||||
).map(s => s.studentnumber)
|
||||
|
||||
const notTransferredStudents = studentnumberlist.filter(sn => !transferredOutStudents.includes(sn))
|
||||
return notTransferredStudents
|
||||
studentnumberlist = studentnumberlist.filter(sn => !transfersOut.includes(sn))
|
||||
}
|
||||
|
||||
// fetch students that have transferred to the programme and filter out these studentnumbers
|
||||
if (transferredToStudents) {
|
||||
const transfersTo = (
|
||||
await Transfer.findAll({
|
||||
attributes: ['studentnumber'],
|
||||
where: {
|
||||
targetcode: {
|
||||
[Op.in]: studyRights,
|
||||
},
|
||||
transferdate: {
|
||||
[Op.gt]: startDate,
|
||||
},
|
||||
studentnumber: {
|
||||
[Op.in]: studentnumberlist,
|
||||
},
|
||||
},
|
||||
raw: true,
|
||||
})
|
||||
).map(s => s.studentnumber)
|
||||
studentnumberlist = studentnumberlist.filter(sn => !transfersTo.includes(sn))
|
||||
}
|
||||
|
||||
// fetch students that have graduated from the programme and filter out these studentnumbers
|
||||
if (graduatedStudents) {
|
||||
const graduated = (
|
||||
await Student.findAll({
|
||||
attributes: ['studentnumber'],
|
||||
include: [
|
||||
{
|
||||
model: Studyright,
|
||||
include: [
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: studyRights,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
graduated: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
studentnumber: {
|
||||
[Op.in]: studentnumbers,
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(s => s.studentnumber)
|
||||
studentnumberlist = studentnumberlist.filter(sn => !graduated.includes(sn))
|
||||
}
|
||||
|
||||
return studentnumberlist
|
||||
@@ -535,13 +634,11 @@ const parseQueryParams = query => {
|
||||
? `${moment(year, 'YYYY').add(1, 'years').format('YYYY')}-${semesterEnd[semesters.find(s => s === 'SPRING')]}`
|
||||
: `${year}-${semesterEnd[semesters.find(s => s === 'FALL')]}`
|
||||
const exchangeStudents = studentStatuses && studentStatuses.includes('EXCHANGE')
|
||||
const cancelledStudents = studentStatuses && studentStatuses.includes('CANCELLED')
|
||||
const nondegreeStudents = studentStatuses && studentStatuses.includes('NONDEGREE')
|
||||
const transferredStudents = studentStatuses && studentStatuses.includes('TRANSFERRED')
|
||||
|
||||
return {
|
||||
exchangeStudents,
|
||||
cancelledStudents,
|
||||
nondegreeStudents,
|
||||
transferredStudents,
|
||||
// if someone passes something falsy like null as the studyright, remove it so it doesn't break
|
||||
@@ -646,7 +743,7 @@ const getOptionsForStudents = async (students, code, level) => {
|
||||
}
|
||||
|
||||
const formatStudentsForApi = async (
|
||||
{ students, credits, extents, semesters, elementdetails, courses },
|
||||
{ students, enrollments, credits, extents, semesters, elementdetails, courses },
|
||||
startDate,
|
||||
endDate,
|
||||
{ studyRights },
|
||||
@@ -667,6 +764,11 @@ const formatStudentsForApi = async (
|
||||
acc[e.student_studentnumber].push(e)
|
||||
return acc
|
||||
}, {})
|
||||
enrollments = enrollments.reduce((acc, e) => {
|
||||
acc[e.studentnumber] = acc[e.studentnumber] || []
|
||||
acc[e.studentnumber].push(e)
|
||||
return acc
|
||||
}, {})
|
||||
const result = students.reduce(
|
||||
(stats, student) => {
|
||||
student.transfers.forEach(transfer => {
|
||||
@@ -680,7 +782,15 @@ const formatStudentsForApi = async (
|
||||
if (student.studentnumber in optionData) student.option = optionData[student.studentnumber]
|
||||
else student.option = null
|
||||
stats.students.push(
|
||||
formatStudentForPopulationStatistics(student, credits, startDate, endDate, startDateMoment, endDateMoment)
|
||||
formatStudentForPopulationStatistics(
|
||||
student,
|
||||
enrollments,
|
||||
credits,
|
||||
startDate,
|
||||
endDate,
|
||||
startDateMoment,
|
||||
endDateMoment
|
||||
)
|
||||
)
|
||||
return stats
|
||||
},
|
||||
@@ -743,22 +853,13 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
|
||||
if (
|
||||
formattedQueryParams.studentStatuses &&
|
||||
!formattedQueryParams.studentStatuses.every(
|
||||
status => status === 'CANCELLED' || status === 'EXCHANGE' || status === 'NONDEGREE' || status === 'TRANSFERRED'
|
||||
status => status === 'EXCHANGE' || status === 'NONDEGREE' || status === 'TRANSFERRED'
|
||||
)
|
||||
) {
|
||||
return { error: 'Student status should be either CANCELLED or EXCHANGE or NONDEGREE or TRANSFERRED' }
|
||||
return { error: 'Student status should be either EXCHANGE or NONDEGREE or TRANSFERRED' }
|
||||
}
|
||||
const {
|
||||
studyRights,
|
||||
startDate,
|
||||
months,
|
||||
endDate,
|
||||
exchangeStudents,
|
||||
cancelledStudents,
|
||||
nondegreeStudents,
|
||||
transferredStudents,
|
||||
tag,
|
||||
} = parseQueryParams(formattedQueryParams)
|
||||
const { studyRights, startDate, months, endDate, exchangeStudents, nondegreeStudents, transferredStudents, tag } =
|
||||
parseQueryParams(formattedQueryParams)
|
||||
|
||||
// db startdate is formatted to utc so need to change it when querying
|
||||
const formattedStartDate = new Date(moment.tz(startDate, 'Europe/Helsinki').format()).toUTCString()
|
||||
@@ -770,7 +871,6 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
|
||||
formattedStartDate,
|
||||
endDate,
|
||||
exchangeStudents,
|
||||
cancelledStudents,
|
||||
nondegreeStudents,
|
||||
transferredStudents
|
||||
)
|
||||
@@ -784,48 +884,89 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
|
||||
optionData = await getOptionsForStudents(studentnumbers, code, 'BSC')
|
||||
}
|
||||
|
||||
const { students, credits, extents, semesters, elementdetails, courses } = await getStudentsIncludeCoursesBetween(
|
||||
studentnumbers,
|
||||
startDate,
|
||||
dateMonthsFromNow(startDate, months),
|
||||
studyRights,
|
||||
tag
|
||||
)
|
||||
const { students, enrollments, credits, extents, semesters, elementdetails, courses } =
|
||||
await getStudentsIncludeCoursesBetween(
|
||||
studentnumbers,
|
||||
startDate,
|
||||
dateMonthsFromNow(startDate, months),
|
||||
studyRights,
|
||||
tag
|
||||
)
|
||||
|
||||
const formattedStudents = await formatStudentsForApi(
|
||||
{ students, credits, extents, semesters, elementdetails, courses },
|
||||
{ students, enrollments, credits, extents, semesters, elementdetails, courses },
|
||||
startDate,
|
||||
endDate,
|
||||
formattedQueryParams,
|
||||
optionData
|
||||
)
|
||||
|
||||
return formattedStudents
|
||||
}
|
||||
|
||||
const findCourses = async (studentnumbers, beforeDate) => {
|
||||
const res = await Course.findAll({
|
||||
attributes: ['code', 'name', 'coursetypecode', 'substitutions'],
|
||||
include: [
|
||||
{
|
||||
required: true,
|
||||
model: Credit,
|
||||
attributes: ['grade', 'student_studentnumber', 'credittypecode', 'attainment_date', 'course_code'],
|
||||
where: {
|
||||
student_studentnumber: {
|
||||
[Op.in]: studentnumbers,
|
||||
},
|
||||
attainment_date: {
|
||||
[Op.lt]: beforeDate,
|
||||
},
|
||||
},
|
||||
const getSubstitutions = async codes => {
|
||||
const courses = await Course.findAll({ where: { code: codes }, attributes: ['code', 'substitutions'], raw: true })
|
||||
return [...new Set(courses.map(({ code, substitutions }) => [code, ...substitutions]).flat())]
|
||||
}
|
||||
|
||||
const findCourses = async (studentnumbers, beforeDate, courses = [], studentCountLimit = 0) => {
|
||||
const courseCodes = courses.length > 0 ? await getSubstitutions(courses) : ['DUMMY']
|
||||
|
||||
const res = await sequelize.query(
|
||||
`
|
||||
SELECT DISTINCT ON (course.id)
|
||||
course.code,
|
||||
course.name,
|
||||
course.coursetypecode,
|
||||
course.substitutions,
|
||||
course.main_course_code,
|
||||
course_types.name AS course_type,
|
||||
credit.data AS credits,
|
||||
enrollment.data AS enrollments
|
||||
FROM course
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
course_code,
|
||||
ARRAY_AGG(JSON_BUILD_OBJECT(
|
||||
'grade', grade,
|
||||
'student_studentnumber', student_studentnumber,
|
||||
'attainment_date', attainment_date,
|
||||
'credittypecode', credittypecode,
|
||||
'course_code', course_code
|
||||
)) AS data
|
||||
FROM credit
|
||||
WHERE student_studentnumber IN (:studentnumbers) AND attainment_date < :beforeDate
|
||||
GROUP BY credit.course_code
|
||||
HAVING COUNT(DISTINCT credit.student_studentnumber) >= :studentCountLimit
|
||||
) credit ON credit.course_code = course.code
|
||||
INNER JOIN course_types ON course_types.coursetypecode = course.coursetypecode
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
course_id,
|
||||
ARRAY_AGG(JSON_BUILD_OBJECT(
|
||||
'studentnumber', studentnumber,
|
||||
'state', state,
|
||||
'enrollment_date_time', enrollment_date_time
|
||||
)) AS data
|
||||
FROM enrollment
|
||||
WHERE enrollment.studentnumber IN (:studentnumbers) AND enrollment.enrollment_date_time < :beforeDate
|
||||
GROUP BY enrollment.course_id
|
||||
) enrollment ON enrollment.course_id = course.id
|
||||
WHERE :skipCourseCodeFilter OR course.code IN (:courseCodes)
|
||||
-- GROUP BY 1, 2, 3, 4, 5, 6
|
||||
`,
|
||||
{
|
||||
replacements: {
|
||||
studentnumbers: studentnumbers.length > 0 ? studentnumbers : ['DUMMY'],
|
||||
beforeDate,
|
||||
studentCountLimit,
|
||||
courseCodes,
|
||||
skipCourseCodeFilter: courses.length === 0,
|
||||
},
|
||||
{
|
||||
attributes: ['coursetypecode', 'name'],
|
||||
model: CourseType,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
}
|
||||
)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -841,38 +982,9 @@ const parseCreditInfo = credit => ({
|
||||
date: credit.attainment_date,
|
||||
})
|
||||
|
||||
// const isOpenUniCourseCode = code => code.match(/^AY?(.+?)(?:en|fi|sv)?$/)
|
||||
|
||||
/* const unifyOpenUniversity = code => {
|
||||
const regexresult = isOpenUniCourseCode(code)
|
||||
if (!regexresult) return code
|
||||
return regexresult[1]
|
||||
}
|
||||
*/
|
||||
const findMainCourse = async code => {
|
||||
const course = await Course.findOne({
|
||||
where: { code: code },
|
||||
})
|
||||
const sortedCourses = sortMainCode([...course.substitutions, code])
|
||||
const result = await Course.findOne({
|
||||
attributes: ['code', 'name'],
|
||||
where: { code: sortedCourses[0] },
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const bottlenecksOf = async (query, studentnumberlist) => {
|
||||
const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
|
||||
const isValidRequest = async (query, params) => {
|
||||
const {
|
||||
studyRights,
|
||||
startDate,
|
||||
endDate,
|
||||
exchangeStudents,
|
||||
cancelledStudents,
|
||||
nondegreeStudents,
|
||||
transferredStudents,
|
||||
} = params
|
||||
const { studyRights, startDate, endDate, exchangeStudents, nondegreeStudents, transferredStudents } = params
|
||||
|
||||
if (!query.semesters.every(semester => semester === 'FALL' || semester === 'SPRING')) {
|
||||
return { error: 'Semester should be either SPRING OR FALL' }
|
||||
@@ -880,10 +992,10 @@ const bottlenecksOf = async (query, studentnumberlist) => {
|
||||
if (
|
||||
query.studentStatuses &&
|
||||
!query.studentStatuses.every(
|
||||
status => status === 'CANCELLED' || status === 'EXCHANGE' || status === 'NONDEGREE' || status === 'TRANSFERRED'
|
||||
status => status === 'EXCHANGE' || status === 'NONDEGREE' || status === 'TRANSFERRED'
|
||||
)
|
||||
) {
|
||||
return { error: 'Student status should be either CANCELLED or EXCHANGE or NONDEGREE or TRANSFERRED' }
|
||||
return { error: 'Student status should be either EXCHANGE or NONDEGREE or TRANSFERRED' }
|
||||
}
|
||||
if (query.selectedStudents) {
|
||||
const allStudents = await studentnumbersWithAllStudyrightElements(
|
||||
@@ -891,7 +1003,6 @@ const bottlenecksOf = async (query, studentnumberlist) => {
|
||||
startDate,
|
||||
endDate,
|
||||
exchangeStudents,
|
||||
cancelledStudents,
|
||||
nondegreeStudents,
|
||||
transferredStudents,
|
||||
query.tag
|
||||
@@ -905,19 +1016,10 @@ const bottlenecksOf = async (query, studentnumberlist) => {
|
||||
return null
|
||||
}
|
||||
|
||||
const getStudentsAndCourses = async (selectedStudents, studentnumberlist) => {
|
||||
const getStudentsAndCourses = async (selectedStudents, studentnumberlist, courseCodes) => {
|
||||
if (!studentnumberlist) {
|
||||
const {
|
||||
months,
|
||||
studyRights,
|
||||
startDate,
|
||||
endDate,
|
||||
exchangeStudents,
|
||||
cancelledStudents,
|
||||
nondegreeStudents,
|
||||
transferredStudents,
|
||||
tag,
|
||||
} = params
|
||||
const { months, studyRights, startDate, endDate, exchangeStudents, nondegreeStudents, transferredStudents, tag } =
|
||||
params
|
||||
const studentnumbers =
|
||||
selectedStudents ||
|
||||
(await studentnumbersWithAllStudyrightElements(
|
||||
@@ -925,7 +1027,6 @@ const bottlenecksOf = async (query, studentnumberlist) => {
|
||||
startDate,
|
||||
endDate,
|
||||
exchangeStudents,
|
||||
cancelledStudents,
|
||||
nondegreeStudents,
|
||||
transferredStudents,
|
||||
tag
|
||||
@@ -936,20 +1037,44 @@ const bottlenecksOf = async (query, studentnumberlist) => {
|
||||
return numbers
|
||||
}, {})
|
||||
|
||||
const courses = await findCourses(studentnumbers, dateMonthsFromNow(startDate, months))
|
||||
const courses = await findCourses(
|
||||
studentnumbers,
|
||||
dateMonthsFromNow(startDate, months),
|
||||
courseCodes,
|
||||
query.studentCountLimit
|
||||
)
|
||||
return [allstudents, courses]
|
||||
} else {
|
||||
const { months, startDate } = params
|
||||
const beforeDate = months && startDate ? dateMonthsFromNow(startDate, months) : new Date()
|
||||
|
||||
const allstudents = studentnumberlist.reduce((numbers, num) => {
|
||||
numbers[num] = true
|
||||
return numbers
|
||||
}, {})
|
||||
const courses = await findCourses(studentnumberlist, new Date())
|
||||
const courses = await findCourses(studentnumberlist, beforeDate, courseCodes, query.studentCountLimit)
|
||||
return [allstudents, courses]
|
||||
}
|
||||
}
|
||||
|
||||
const encryptStudentnumbers = bottlenecks => {
|
||||
for (const course in bottlenecks.coursestatistics) {
|
||||
const encryptedStudentStats = {}
|
||||
for (const data in bottlenecks.coursestatistics[course].students) {
|
||||
encryptedStudentStats[data] = {}
|
||||
const studentnumbers = Object.keys(bottlenecks.coursestatistics[course].students[data])
|
||||
studentnumbers.forEach(studentnumber => {
|
||||
encryptedStudentStats[data][encrypt(studentnumber).encryptedData] =
|
||||
bottlenecks.coursestatistics[course].students[data][studentnumber]
|
||||
})
|
||||
}
|
||||
bottlenecks.coursestatistics[course].students = encryptedStudentStats
|
||||
}
|
||||
}
|
||||
|
||||
const params = parseQueryParams(query)
|
||||
const [[allstudents, courses], error] = await Promise.all([
|
||||
getStudentsAndCourses(query.selectedStudents, studentnumberlist),
|
||||
getStudentsAndCourses(query.selectedStudents, studentnumberlist, query.courses),
|
||||
isValidRequest(query, params),
|
||||
])
|
||||
if (error) return error
|
||||
@@ -963,17 +1088,19 @@ const bottlenecksOf = async (query, studentnumberlist) => {
|
||||
|
||||
const startYear = parseInt(query.year, 10)
|
||||
const allstudentslength = Object.keys(allstudents).length
|
||||
const coursesByCode = keyBy(courses, 'code')
|
||||
|
||||
for (const course of courses) {
|
||||
let { course_type } = course
|
||||
let maincourse = course
|
||||
const sortedMaincourse = await findMainCourse(course.code)
|
||||
if (sortedMaincourse) maincourse = sortedMaincourse
|
||||
|
||||
/* if (isOpenUniCourseCode) {
|
||||
const nonOpen = unifyOpenUniversity(course.code)
|
||||
const response = await findOneByCode(nonOpen)
|
||||
*/
|
||||
if (course.main_course_code && course.main_course_code !== course.code) {
|
||||
let newmain = coursesByCode[course.main_course_code]
|
||||
|
||||
if (newmain) {
|
||||
maincourse = newmain
|
||||
}
|
||||
}
|
||||
|
||||
if (!stats[maincourse.code]) {
|
||||
stats[maincourse.code] = new CourseStatsCounter(maincourse.code, maincourse.name, allstudentslength)
|
||||
@@ -990,12 +1117,22 @@ const bottlenecksOf = async (query, studentnumberlist) => {
|
||||
const semester = getPassingSemester(startYear, date)
|
||||
coursestats.markCredit(studentnumber, grade, passingGrade, failingGrade, improvedGrade, semester)
|
||||
})
|
||||
|
||||
if (course.enrollments) {
|
||||
course.enrollments.forEach(({ studentnumber, state, enrollment_date_time }) => {
|
||||
const semester = getPassingSemester(startYear, enrollment_date_time)
|
||||
coursestats.markEnrollment(studentnumber, state, semester)
|
||||
})
|
||||
}
|
||||
|
||||
stats[maincourse.code] = coursestats
|
||||
}
|
||||
|
||||
bottlenecks.coursestatistics = Object.values(stats).map(coursestatistics => coursestatistics.getFinalStats())
|
||||
bottlenecks.allStudents = allstudentslength
|
||||
|
||||
if (encryptdata) encryptStudentnumbers(bottlenecks)
|
||||
|
||||
return bottlenecks
|
||||
}
|
||||
|
||||
|
||||
@@ -38,8 +38,14 @@ const byProgrammeCode = async code => {
|
||||
},
|
||||
})
|
||||
|
||||
// filter out possible duplicates
|
||||
const filtered = result.filter(
|
||||
(course, index, array) =>
|
||||
array.findIndex(c => c.id == course.id && c.code == course.code && c.label_code == course.label_code) == index
|
||||
)
|
||||
|
||||
// this labels the modules to match the old system in frontend
|
||||
const labeled = result.map(module => {
|
||||
const labeled = filtered.map(module => {
|
||||
const label = {
|
||||
id: module.label_name.fi,
|
||||
label: `${module.label_code}\n${module.label_name.fi}`,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const moment = require('moment')
|
||||
const sequelize = require('sequelize')
|
||||
const { Semester } = require('../models')
|
||||
const Op = sequelize.Op
|
||||
@@ -28,8 +29,8 @@ const getSemestersAndYears = async () => {
|
||||
acc.years[yearcode] = {
|
||||
yearcode,
|
||||
yearname,
|
||||
startdate: Math.min(acc.years[yearcode].startdate, startdate),
|
||||
enddate: Math.max(acc.years[yearcode].enddate, enddate),
|
||||
startdate: moment.min(moment(acc.years[yearcode].startdate), moment(startdate)),
|
||||
enddate: moment.max(moment(acc.years[yearcode].enddate), moment(enddate)),
|
||||
}
|
||||
}
|
||||
return acc
|
||||
|
||||
@@ -26,7 +26,7 @@ const populationCourseStatsMerger = multiyearstats => {
|
||||
stats.coursestatistics[index].stats.retryPassed += c.stats.retryPassed
|
||||
|
||||
stats.coursestatistics[index].stats.perStudent =
|
||||
(stats.coursestatistics[index].stats.attempts / stats.coursestatistics[index].stats.students) * 100
|
||||
stats.coursestatistics[index].stats.attempts / stats.coursestatistics[index].stats.students
|
||||
|
||||
stats.coursestatistics[index].stats.percentage =
|
||||
(stats.coursestatistics[index].stats.passed / stats.coursestatistics[index].stats.students) * 100
|
||||
|
||||
@@ -12,6 +12,7 @@ const {
|
||||
ElementDetail,
|
||||
SemesterEnrollment,
|
||||
Semester,
|
||||
Studyplan,
|
||||
} = require('../models')
|
||||
const { TagStudent, Tag } = require('../models/models_kone')
|
||||
const Op = Sequelize.Op
|
||||
@@ -57,6 +58,10 @@ const byId = async id => {
|
||||
{
|
||||
model: SemesterEnrollment,
|
||||
},
|
||||
{
|
||||
model: Studyplan,
|
||||
attributes: ['included_courses', 'programme_code', 'completed_credits'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
TagStudent.findAll({
|
||||
@@ -99,33 +104,79 @@ const byId = async id => {
|
||||
return student
|
||||
}
|
||||
|
||||
const findByCourseAndSemesters = async (coursecodes, from, to, separate) =>
|
||||
(
|
||||
const getUnifyStatus = unifyCourses => {
|
||||
switch (unifyCourses) {
|
||||
case 'unifyStats':
|
||||
return [true, false]
|
||||
|
||||
case 'openStats':
|
||||
return [true]
|
||||
|
||||
case 'reqularStats':
|
||||
return [false]
|
||||
|
||||
default:
|
||||
return [true, false]
|
||||
}
|
||||
}
|
||||
|
||||
const findByCourseAndSemesters = async (coursecodes, from, to, separate, unifyCourses = 'unifyStats') => {
|
||||
const { startdate, semestercode: fromSemester } = await Semester.findOne({
|
||||
where: {
|
||||
[separate ? 'semestercode' : 'yearcode']: from,
|
||||
},
|
||||
order: ['semestercode'],
|
||||
limit: 1,
|
||||
raw: true,
|
||||
})
|
||||
const { enddate, semestercode: toSemester } = await Semester.findOne({
|
||||
where: {
|
||||
[separate ? 'semestercode' : 'yearcode']: to,
|
||||
},
|
||||
order: [['semestercode', 'DESC']],
|
||||
limit: 1,
|
||||
raw: true,
|
||||
})
|
||||
return (
|
||||
await sequelize.query(
|
||||
`
|
||||
SELECT
|
||||
studentnumber
|
||||
FROM student
|
||||
INNER JOIN credit ON
|
||||
student.studentnumber=credit.student_studentnumber
|
||||
FROM
|
||||
student s
|
||||
WHERE
|
||||
course_code IN (:coursecodes) AND
|
||||
attainment_date
|
||||
BETWEEN
|
||||
(select startdate FROM semesters where ${
|
||||
separate ? 'semestercode' : 'yearcode'
|
||||
}=:minYearCode ORDER BY semestercode LIMIT 1) AND
|
||||
(select enddate FROM semesters where ${
|
||||
separate ? 'semestercode' : 'yearcode'
|
||||
}=:maxYearCode ORDER BY semestercode DESC LIMIT 1);
|
||||
EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
credit c
|
||||
WHERE
|
||||
c.student_studentnumber = s.studentnumber
|
||||
AND c.course_code IN (:coursecodes)
|
||||
AND c.is_open IN (:isOpen)
|
||||
AND c.attainment_date BETWEEN '${startdate.toISOString()}' AND '${enddate.toISOString()}'
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
enrollment e
|
||||
WHERE
|
||||
e.studentnumber = s.studentnumber
|
||||
AND e.course_code IN (:coursecodes)
|
||||
AND e.semestercode BETWEEN ${fromSemester} AND ${toSemester}
|
||||
AND e.enrollment_date_time >= '2021-05-31'
|
||||
AND e.state IN ('ENROLLED', 'CONFIRMED')
|
||||
);
|
||||
`,
|
||||
{
|
||||
replacements: { coursecodes, minYearCode: from, maxYearCode: to },
|
||||
replacements: { coursecodes, minYearCode: from, maxYearCode: to, isOpen: getUnifyStatus(unifyCourses) },
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
raw: true,
|
||||
}
|
||||
)
|
||||
).map(st => st.studentnumber)
|
||||
}
|
||||
|
||||
const findByTag = async tag => {
|
||||
return (
|
||||
@@ -140,7 +191,7 @@ const findByTag = async tag => {
|
||||
).map(st => st.studentnumber)
|
||||
}
|
||||
|
||||
const formatStudent = ({
|
||||
const formatStudent = async ({
|
||||
firstnames,
|
||||
lastname,
|
||||
studentnumber,
|
||||
@@ -153,11 +204,13 @@ const formatStudent = ({
|
||||
studyrights,
|
||||
semester_enrollments,
|
||||
transfers,
|
||||
studyplans,
|
||||
updatedAt,
|
||||
createdAt,
|
||||
tags,
|
||||
sis_person_id,
|
||||
}) => {
|
||||
const toCourse = ({ id, grade, credits, credittypecode, attainment_date, course, isStudyModule }) => {
|
||||
const toCourse = ({ id, grade, credits, credittypecode, is_open, attainment_date, course, isStudyModule }) => {
|
||||
try {
|
||||
course = course.get()
|
||||
} catch (e) {
|
||||
@@ -183,18 +236,21 @@ const formatStudent = ({
|
||||
credits,
|
||||
credittypecode,
|
||||
isStudyModuleCredit: isStudyModule,
|
||||
isOpenCourse: is_open,
|
||||
}
|
||||
}
|
||||
|
||||
studyrights = studyrights || []
|
||||
semester_enrollments = semester_enrollments || []
|
||||
studyplans = studyplans || []
|
||||
const semesterenrollments = semester_enrollments.map(
|
||||
({ semestercode, enrollmenttype, name, yearname, startYear }) => ({
|
||||
({ semestercode, enrollmenttype, name, yearname, startYear, statutory_absence: statutoryAbsence }) => ({
|
||||
yearname,
|
||||
name,
|
||||
startYear,
|
||||
semestercode,
|
||||
enrollmenttype,
|
||||
statutoryAbsence,
|
||||
})
|
||||
)
|
||||
|
||||
@@ -211,6 +267,13 @@ const formatStudent = ({
|
||||
.map(toCourse)
|
||||
.filter(c => c.course.name !== 'missing')
|
||||
|
||||
const hopsCourses = studyplans.map(sp => sp.included_courses).flat()
|
||||
|
||||
const allCourses = await sequelize.query(
|
||||
'SELECT DISTINCT ON(course_code) course_code, credits FROM credit WHERE course_code IN (:hopsCourses)',
|
||||
{ replacements: { hopsCourses: hopsCourses.concat('id-to-fill-array') } }
|
||||
)
|
||||
|
||||
return {
|
||||
firstnames,
|
||||
lastname,
|
||||
@@ -225,14 +288,17 @@ const formatStudent = ({
|
||||
email,
|
||||
semesterenrollments,
|
||||
updatedAt: updatedAt || createdAt,
|
||||
studyplans,
|
||||
allCourses: allCourses[0],
|
||||
tags,
|
||||
sis_person_id: sis_person_id,
|
||||
}
|
||||
}
|
||||
|
||||
const withId = async id => {
|
||||
try {
|
||||
const result = await byId(id)
|
||||
return formatStudent(result)
|
||||
return await formatStudent(result)
|
||||
} catch (e) {
|
||||
logger.error({ message: 'error when fetching single student', meta: e })
|
||||
return {
|
||||
@@ -285,59 +351,74 @@ const bySearchTerm = async searchterm => {
|
||||
[Op.or]: [nameLike(terms), studentnumberLike(terms)],
|
||||
},
|
||||
})
|
||||
return matches.map(formatStudent)
|
||||
return await Promise.all(matches.map(async s => await formatStudent(s)))
|
||||
}
|
||||
|
||||
const bySearchTermAndElements = async (searchterm, codes) => {
|
||||
const bySearchTermAndStudentNumbers = async (searchterm, studentnumbers) => {
|
||||
const terms = splitByEmptySpace(searchterm)
|
||||
const matches = await Student.findAll({
|
||||
include: {
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: codes,
|
||||
where: {
|
||||
[Op.and]: {
|
||||
[Op.or]: [nameLike(terms), studentnumberLike(terms)],
|
||||
studentnumber: {
|
||||
[Op.in]: studentnumbers,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
[Op.or]: [nameLike(terms), studentnumberLike(terms)],
|
||||
},
|
||||
})
|
||||
return matches.map(formatStudent)
|
||||
return await Promise.all(matches.map(async s => await formatStudent(s)))
|
||||
}
|
||||
|
||||
const filterStudentnumbersByAccessrights = async (studentnumbers, codes) => {
|
||||
const students = await Student.findAll({
|
||||
attributes: ['studentnumber'],
|
||||
include: {
|
||||
attributes: [],
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: codes,
|
||||
const filterStudentnumbersByAccessrights = async (studentnumbers, codes) =>
|
||||
(
|
||||
await Student.findAll({
|
||||
attributes: ['studentnumber'],
|
||||
include: {
|
||||
attributes: [],
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: codes,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
studentnumber: {
|
||||
[Op.in]: studentnumbers,
|
||||
where: {
|
||||
studentnumber: {
|
||||
[Op.in]: studentnumbers,
|
||||
},
|
||||
},
|
||||
},
|
||||
raw: true,
|
||||
})
|
||||
return students.map(student => student.studentnumber)
|
||||
}
|
||||
raw: true,
|
||||
})
|
||||
).map(({ studentnumber }) => studentnumber)
|
||||
|
||||
const getStudentnumbersByElementdetails = async codes =>
|
||||
(
|
||||
await Student.findAll({
|
||||
attributes: ['studentnumber'],
|
||||
include: {
|
||||
attributes: [],
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: codes,
|
||||
},
|
||||
},
|
||||
},
|
||||
raw: true,
|
||||
})
|
||||
).map(({ studentnumber }) => studentnumber)
|
||||
|
||||
module.exports = {
|
||||
withId,
|
||||
bySearchTerm,
|
||||
createStudent,
|
||||
updateStudent,
|
||||
bySearchTermAndElements,
|
||||
bySearchTermAndStudentNumbers,
|
||||
filterStudentnumbersByAccessrights,
|
||||
findByCourseAndSemesters,
|
||||
findByTag,
|
||||
splitByEmptySpace,
|
||||
getStudentnumbersByElementdetails,
|
||||
}
|
||||
|
||||
@@ -2,42 +2,42 @@ const { getImporterClient } = require('../util/importerClient')
|
||||
const importerClient = getImporterClient()
|
||||
const { StudyGuidanceGroupTag } = require('../models/models_kone')
|
||||
const logger = require('../util/logger')
|
||||
const _ = require('lodash')
|
||||
|
||||
const getAllStudentsUserHasInGroups = async sisPersonId => {
|
||||
const getGroupsFromImporter = async sisPersonId => {
|
||||
if (!importerClient) {
|
||||
return new Set()
|
||||
return []
|
||||
}
|
||||
const answerTimeout = new Promise(resolve => setTimeout(resolve, 2000))
|
||||
try {
|
||||
const { data } = await importerClient.get(`/person-groups/person/${sisPersonId}`)
|
||||
return new Set(
|
||||
Object.values(data)
|
||||
.map(group => group.members)
|
||||
.flat()
|
||||
.map(member => member.personStudentNumber)
|
||||
)
|
||||
const result = await Promise.race([importerClient.get(`/person-groups/person/${sisPersonId}`), answerTimeout])
|
||||
if (!result) return []
|
||||
return Object.values(result.data)
|
||||
} catch (error) {
|
||||
logger.error("Couldn't fetch users studyguidance groups")
|
||||
return new Set()
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const getAllGroupsAndStudents = async sisPersonId => {
|
||||
if (!importerClient) {
|
||||
return []
|
||||
}
|
||||
try {
|
||||
const { data } = await importerClient.get(`/person-groups/person/${sisPersonId}`)
|
||||
const tagsByGroupId = (await StudyGuidanceGroupTag.findAll()).reduce((acc, curr) => {
|
||||
const { studyProgramme, year } = curr
|
||||
return { ...acc, [curr.studyGuidanceGroupId]: { studyProgramme, year } }
|
||||
}, {})
|
||||
return Object.values(data).map(group => ({ ...group, tags: tagsByGroupId[group.id] }))
|
||||
} catch (error) {
|
||||
logger.error("Couldn't fetch users studyguidance groups")
|
||||
return []
|
||||
}
|
||||
const tagsByGroupId = (await StudyGuidanceGroupTag.findAll()).reduce((acc, curr) => {
|
||||
const { studyProgramme, year, studyGuidanceGroupId } = curr
|
||||
return { ...acc, [studyGuidanceGroupId]: { studyProgramme, year } }
|
||||
}, {})
|
||||
return (await getGroupsFromImporter(sisPersonId)).map(group => ({
|
||||
...group,
|
||||
tags: tagsByGroupId[group.id],
|
||||
}))
|
||||
}
|
||||
|
||||
const getAllStudentsUserHasInGroups = async sisPersonId =>
|
||||
_.uniqBy(
|
||||
(await getGroupsFromImporter(sisPersonId))
|
||||
.map(group => group.members)
|
||||
.flat()
|
||||
.map(member => member.personStudentNumber)
|
||||
)
|
||||
|
||||
const changeGroupTags = async ({ groupId, tags }) => {
|
||||
const { studyProgramme, year } = tags
|
||||
const tagToUpdate = studyProgramme ? { studyProgramme } : { year }
|
||||
@@ -56,8 +56,11 @@ const changeGroupTags = async ({ groupId, tags }) => {
|
||||
return result
|
||||
}
|
||||
|
||||
const checkStudyGuidanceGroupsAccess = async hyPersonSisuId => (await getGroupsFromImporter(hyPersonSisuId)).length > 0
|
||||
|
||||
module.exports = {
|
||||
getAllStudentsUserHasInGroups,
|
||||
getAllGroupsAndStudents,
|
||||
changeGroupTags,
|
||||
checkStudyGuidanceGroupsAccess,
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user