Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f8b6133e5 | ||
|
|
1118d2b2da | ||
|
|
d8dcf1608d | ||
|
|
592832eb95 | ||
|
|
dba5045982 | ||
|
|
2513986361 | ||
|
|
e7dee7f139 | ||
|
|
fdcc7648c5 | ||
|
|
b3ddbf669a | ||
|
|
ca00efffd4 | ||
|
|
b73850c8c6 | ||
|
|
db86b1a8d3 | ||
|
|
6c27c11356 | ||
|
|
5e4f5b5e7a | ||
|
|
e9b7c677f8 | ||
|
|
7fa8ec5176 | ||
|
|
027899234f | ||
|
|
5e223eb749 | ||
|
|
cf5049db05 | ||
|
|
d20db4feb4 | ||
|
|
d214fe0282 | ||
|
|
492ad09e36 | ||
|
|
f62cb9718c | ||
|
|
a6ac9493d6 | ||
|
|
93f1c3dc99 | ||
|
|
853c4ceba5 | ||
|
|
d485fc604d | ||
|
|
68af9ab38c | ||
|
|
899d5e4e43 | ||
|
|
4aadf329ea | ||
|
|
23ebb69778 | ||
|
|
855ca8e134 | ||
|
|
9b21a4a1d4 | ||
|
|
c083803858 | ||
|
|
62735ab481 | ||
|
|
cd252469e3 | ||
|
|
4ff5e42752 | ||
|
|
d755aaab92 | ||
|
|
b98b03b4a0 | ||
|
|
6d7dfad89a | ||
|
|
6070486745 | ||
|
|
f5ee0211a4 | ||
|
|
49e1b3e99d | ||
|
|
d5256f9b39 | ||
|
|
3bdf76ed03 | ||
|
|
cc692f34c3 | ||
|
|
98a56eead4 | ||
|
|
b823d0f23a | ||
|
|
a3648898e6 | ||
|
|
0b64a9a11f | ||
|
|
4bb61d0784 | ||
|
|
71bc53d787 | ||
|
|
005107882c | ||
|
|
367376065e | ||
|
|
45b72cef56 | ||
|
|
60ff7a210d | ||
|
|
0c7de151ba | ||
|
|
32d7ef4534 | ||
|
|
5bb39442b0 | ||
|
|
a8afade3b2 | ||
|
|
fb3acf1f34 | ||
|
|
0a7db609d2 | ||
|
|
886f84d6df | ||
|
|
5e22c82ca0 | ||
|
|
820c1d7a2e | ||
|
|
6a68350203 | ||
|
|
a71c6d51f1 | ||
|
|
bf93df66fe | ||
|
|
de62e7f528 | ||
|
|
854c8e5228 | ||
|
|
150d06e27b | ||
|
|
28ea80b1d0 | ||
|
|
95a97f2c37 | ||
|
|
ba35393c7d | ||
|
|
c5c5889791 | ||
|
|
75968b0b16 | ||
|
|
30b9c33f21 | ||
|
|
fa9324431d | ||
|
|
e2dc26d4af | ||
|
|
12a405ed08 | ||
|
|
dea2db5497 | ||
|
|
925e6646f0 | ||
|
|
09889ba66f | ||
|
|
1332a0ad29 | ||
|
|
281e142d5c | ||
|
|
ddac6ea36c | ||
|
|
2aedf1efe8 | ||
|
|
9edca0fc50 | ||
|
|
6bad2bc5dd | ||
|
|
a1365406ae | ||
|
|
8a66030f3b | ||
|
|
ae82cfb333 | ||
|
|
4f9b87b591 | ||
|
|
508d9d0479 | ||
|
|
3f83d6caaa | ||
|
|
1071d085c7 | ||
|
|
e92726c06a | ||
|
|
e4831c16d8 | ||
|
|
e231a2046f | ||
|
|
4ea39bd13b | ||
|
|
9ed0576989 | ||
|
|
c64ebfdf46 | ||
|
|
6bd7eed960 | ||
|
|
0f65cf014b | ||
|
|
97b5879660 | ||
|
|
a75e333de8 | ||
|
|
a97e17affa | ||
|
|
ddc682850f | ||
|
|
f16ee68853 | ||
|
|
07d43e9efd | ||
|
|
79119477b3 | ||
|
|
224feb18d6 | ||
|
|
d96411fe69 | ||
|
|
843fe84c13 | ||
|
|
5a52e03d52 | ||
|
|
26dfa2dcec | ||
|
|
75c6fb4739 | ||
|
|
dcdf6f5848 | ||
|
|
e32a32ad54 | ||
|
|
238557ebfb | ||
|
|
ddee43a6a3 | ||
|
|
42edf79a39 | ||
|
|
55ee912e56 | ||
|
|
71c6622767 | ||
|
|
1733879a24 | ||
|
|
3542e6df57 | ||
|
|
0948af41f9 | ||
|
|
39af9ac937 | ||
|
|
787c67a5ff | ||
|
|
d6c0847f6a | ||
|
|
febcd20a81 | ||
|
|
34cd554e7a | ||
|
|
be19c456ba | ||
|
|
95e6e38271 | ||
|
|
db9a4b7e78 | ||
|
|
85a52731ad | ||
|
|
a62dfc81b3 | ||
|
|
bef2482759 | ||
|
|
2aaa6b590a | ||
|
|
e64f453386 | ||
|
|
f7a782aea7 | ||
|
|
b32ea59ad5 | ||
|
|
2994c80e97 | ||
|
|
b441a688dc | ||
|
|
547f077538 | ||
|
|
ea4a730ec1 | ||
|
|
b421e8a9eb | ||
|
|
0b4046b061 | ||
|
|
d213b927a4 | ||
|
|
a3d1b992b5 | ||
|
|
d84544d070 | ||
|
|
14646e3bb2 | ||
|
|
64f7bb99ae | ||
|
|
84cdf29e4c | ||
|
|
4262229856 | ||
|
|
5a43e40196 | ||
|
|
daad45264f | ||
|
|
6e081961cb | ||
|
|
93c7c84d87 | ||
|
|
539c77fdde | ||
|
|
022ebb3cc5 | ||
|
|
0e1e2a5959 | ||
|
|
559c659a16 | ||
|
|
8b27e91a83 | ||
|
|
b269dfe995 | ||
|
|
880bce2c03 | ||
|
|
add8e4f7c8 | ||
|
|
4d4fcef3ef | ||
|
|
5bffa46cbf | ||
|
|
d73ae52501 | ||
|
|
448d3668fd | ||
|
|
d69784af32 | ||
|
|
792bf911c0 | ||
|
|
df87abc0ee | ||
|
|
9340efe406 | ||
|
|
3a1e155b11 | ||
|
|
5086734db3 | ||
|
|
0a79ac735f | ||
|
|
255ab0de1f | ||
|
|
14ef484b7a | ||
|
|
1d26819db9 | ||
|
|
bf3a500e58 | ||
|
|
72d846d3b9 | ||
|
|
93ada9edd0 | ||
|
|
1f195e0697 | ||
|
|
aa60d86993 | ||
|
|
b692137207 | ||
|
|
cadffa6b03 | ||
|
|
f804c3295a | ||
|
|
7d8bd40ab3 | ||
|
|
79a2b2ca2c | ||
|
|
a8e6f5ac43 | ||
|
|
43c9c1ad85 | ||
|
|
4109a840cb | ||
|
|
e48e5ba853 | ||
|
|
ddd8ecf5aa | ||
|
|
6f9fdf9f6f | ||
|
|
dacb0cec03 | ||
|
|
1e63739c41 | ||
|
|
6adf5cf583 | ||
|
|
fead93529d | ||
|
|
524c92f098 | ||
|
|
f3197f2d75 | ||
|
|
2adef388ef | ||
|
|
26755bbdfe | ||
|
|
aa71c8cb1f | ||
|
|
5badb32fd8 | ||
|
|
3a0d16aae8 | ||
|
|
d0b30e8c38 | ||
|
|
d41877fe52 | ||
|
|
4f00ca52bd | ||
|
|
09fe88bd7d | ||
|
|
032c9463dc | ||
|
|
6000ac9f3e | ||
|
|
fa1c23a731 | ||
|
|
3d2dd2b77a | ||
|
|
b8e37ce791 | ||
|
|
6a5f8afd4e | ||
|
|
8dcc9e60a7 | ||
|
|
1a66308148 | ||
|
|
a1a4f5e426 | ||
|
|
892f428ee2 | ||
|
|
e630693376 | ||
|
|
1af902569c | ||
|
|
ea84c668c7 | ||
|
|
fd7a34fd39 | ||
|
|
92aa1d5120 | ||
|
|
ee43ec89ac | ||
|
|
c88029c5a1 | ||
|
|
3dd40cbec9 | ||
|
|
7d9076317c | ||
|
|
bdc3dd19c4 | ||
|
|
fb728d5796 | ||
|
|
a20887e5ca | ||
|
|
1703712e48 | ||
|
|
583685f8af | ||
|
|
ea21b08a9e | ||
|
|
85c662c939 | ||
|
|
04c78d4c15 | ||
|
|
a87098210d | ||
|
|
20cc1c8407 | ||
|
|
ad7cb80ca7 | ||
|
|
dfc6ab4739 | ||
|
|
3bbb9916c7 | ||
|
|
fe822d0bd5 | ||
|
|
f1c8e955c5 | ||
|
|
f4cf046958 | ||
|
|
5dfee0c538 | ||
|
|
55f2cca6c9 | ||
|
|
76583491b9 | ||
|
|
78c1ae84ea | ||
|
|
5109174b26 | ||
|
|
34b297de4e | ||
|
|
9f70c7b8f0 | ||
|
|
630fedc9ab | ||
|
|
51a40c75e7 | ||
|
|
a0642b515b | ||
|
|
fc69f74264 | ||
|
|
0d385d003b | ||
|
|
70308afa27 | ||
|
|
1298b90604 | ||
|
|
17c110cf28 | ||
|
|
ae273e846d | ||
|
|
449c1932c6 | ||
|
|
f8a1a547d7 | ||
|
|
61e487bd6b | ||
|
|
13535ba3f6 | ||
|
|
317c4594c3 | ||
|
|
6064fe9514 | ||
|
|
d49eb08bae | ||
|
|
880674b988 | ||
|
|
39a8f9eaee | ||
|
|
9c065e04fb | ||
|
|
a563ee474f | ||
|
|
aebfb0e2c9 | ||
|
|
c036e3b626 | ||
|
|
ef3b8c0baa | ||
|
|
57129b91fe | ||
|
|
451bc495ee | ||
|
|
64f9e61431 | ||
|
|
4d4fa4d0e2 | ||
|
|
8385077ab0 | ||
|
|
74d2baa9f9 | ||
|
|
2916b4153d | ||
|
|
dd484d7207 | ||
|
|
b523aab3b6 | ||
|
|
2bb1af35d3 | ||
|
|
f75fc169a7 | ||
|
|
249fb6f20a | ||
|
|
8bba2d8842 | ||
|
|
a492b14a14 | ||
|
|
5010e65ea9 | ||
|
|
d216069df6 | ||
|
|
47bb1f5d61 | ||
|
|
217bca5153 | ||
|
|
a03e535eb3 | ||
|
|
e5dd44b657 | ||
|
|
f45846b1a3 | ||
|
|
b300a6a131 | ||
|
|
286c778756 | ||
|
|
8f686200ca | ||
|
|
0e2c5462f6 | ||
|
|
e86b327cfe | ||
|
|
6c964ce575 | ||
|
|
7f555e72df | ||
|
|
7178c3ee0d | ||
|
|
9406c6644d | ||
|
|
b70fe4f3bc | ||
|
|
eed77be183 | ||
|
|
afd38f4f91 | ||
|
|
88d3536d6a | ||
|
|
98b7ebe5d5 | ||
|
|
c1eee9f175 | ||
|
|
66ee9b8a3f | ||
|
|
e607d348e2 | ||
|
|
3079039d5d | ||
|
|
8d6a8be91f | ||
|
|
8a124cae9a | ||
|
|
74343af176 | ||
|
|
5e7c8628c3 | ||
|
|
462418c3eb | ||
|
|
8e2b7b2ce1 | ||
|
|
7b6c7aeecb | ||
|
|
b1f34479eb | ||
|
|
56e2462d30 | ||
|
|
f7372e8125 | ||
|
|
a9f6cbe36e | ||
|
|
881e48f76c | ||
|
|
0939e9129a | ||
|
|
cb396623c6 | ||
|
|
8716306083 | ||
|
|
048ba916e4 | ||
|
|
c94549d0d3 | ||
|
|
606ca3bfd5 | ||
|
|
040a4de536 | ||
|
|
ec47d63c1b | ||
|
|
bcad45a84b | ||
|
|
91184ef9d6 | ||
|
|
b946a9e87f | ||
|
|
cdde42e7d5 | ||
|
|
bcb115d648 | ||
|
|
d848030c6a | ||
|
|
c4cf9ba056 | ||
|
|
93aabb97c5 | ||
|
|
603774b287 | ||
|
|
d3e484100a | ||
|
|
9f7f294fe1 | ||
|
|
2be9c3b47c | ||
|
|
79a43aae48 | ||
|
|
4859ab197a | ||
|
|
f155b13995 | ||
|
|
76f2c2a276 | ||
|
|
e66aa244d6 | ||
|
|
cd5af9571a | ||
|
|
9ca473ca8e | ||
|
|
f9b9d7ed53 | ||
|
|
a8ad765dc6 | ||
|
|
7cbc11a3e5 | ||
|
|
2813afb625 | ||
|
|
bbe1ee899a | ||
|
|
bda81c8b39 | ||
|
|
7abd945c5d | ||
|
|
5f332344d6 | ||
|
|
832546cd73 | ||
|
|
a23c638416 | ||
|
|
e9bd6ceae9 | ||
|
|
40079bf963 | ||
|
|
44ffcf878d | ||
|
|
29bd2c8ab5 | ||
|
|
3ca2d6934c | ||
|
|
e75fc3c6ae | ||
|
|
bce4e333c5 | ||
|
|
bc951800d5 | ||
|
|
505ea3254c | ||
|
|
b75bf7cbbf | ||
|
|
da6aeb2cb4 | ||
|
|
3b33c95625 | ||
|
|
5348e86bd2 | ||
|
|
557663e5e1 | ||
|
|
d4073a836c | ||
|
|
e8ad1bb08d | ||
|
|
75195de6cc | ||
|
|
3de924f6a6 | ||
|
|
f9ab5f57fd | ||
|
|
05f7204863 | ||
|
|
75b20c9750 | ||
|
|
346b4a29c7 | ||
|
|
7011849143 | ||
|
|
5ee2c3a9c9 | ||
|
|
5b2649c1bf | ||
|
|
14c1de6a38 | ||
|
|
5a96a08e68 | ||
|
|
4c700b7043 | ||
|
|
a4e1afcc1c | ||
|
|
4ade93b4d7 | ||
|
|
ebd05a11be | ||
|
|
86d2b85e21 | ||
|
|
15e940b77b | ||
|
|
ad562b45b7 | ||
|
|
390ff61162 | ||
|
|
ed44f617c0 | ||
|
|
9e990023b7 | ||
|
|
7575dc85a3 | ||
|
|
c5694840b9 | ||
|
|
81146f9866 | ||
|
|
cedad01fec | ||
|
|
1ea162ef7e | ||
|
|
7e69fcc5ee | ||
|
|
58f69f50f7 | ||
|
|
8d01562b2d | ||
|
|
298f119ad3 | ||
|
|
87f5ffc353 | ||
|
|
05d4927b3c | ||
|
|
9ab9f0ca29 | ||
|
|
6145a76425 | ||
|
|
13c86b3259 | ||
|
|
625ae0cfe3 | ||
|
|
62c8294791 | ||
|
|
d0b7cd8c6c | ||
|
|
65332f5fac | ||
|
|
a6040ceb25 | ||
|
|
c9adca395c | ||
|
|
d33ed0653e | ||
|
|
251f22bf92 | ||
|
|
b7cbac6390 | ||
|
|
0d2a568e8c | ||
|
|
cfa86c5c11 | ||
|
|
81d761202d | ||
|
|
4aec52e21b | ||
|
|
3fcd366888 | ||
|
|
fb66ade27b | ||
|
|
0337e86b34 | ||
|
|
6f92fbdc9a | ||
|
|
ccf4569463 | ||
|
|
1b870de079 | ||
|
|
2f9264b0a0 | ||
|
|
05f182274e | ||
|
|
a74e495ea5 | ||
|
|
9aafd50a66 | ||
|
|
884a4056a3 | ||
|
|
aa46c759e8 | ||
|
|
48001b1f30 | ||
|
|
882d3c5846 | ||
|
|
55f669a86d | ||
|
|
c021c31157 | ||
|
|
7af4cd4668 | ||
|
|
69918b1728 | ||
|
|
35c36cffc6 | ||
|
|
520436e32f | ||
|
|
6b187b8a60 | ||
|
|
c8a93cbf57 | ||
|
|
b4da19e4e6 | ||
|
|
6132ea5f57 | ||
|
|
bebeae8fb6 | ||
|
|
933fbd1689 | ||
|
|
c0165fb02d | ||
|
|
3465051e6b | ||
|
|
09f78e4e38 | ||
|
|
795c00a258 | ||
|
|
9789693dce | ||
|
|
93f95d040c | ||
|
|
e1246113b3 | ||
|
|
f1a5627180 | ||
|
|
993d8fa7be | ||
|
|
719d3ee2c6 | ||
|
|
44f622f306 | ||
|
|
a1f8ed8554 | ||
|
|
ec57728e80 | ||
|
|
054ce51c5f | ||
|
|
ecf4933e79 | ||
|
|
0d96f022d9 | ||
|
|
6044bc70cc | ||
|
|
af8cd065cc | ||
|
|
67020d34ea | ||
|
|
d6c1cb3f49 | ||
|
|
85c315ea6f | ||
|
|
f0aaa6755c | ||
|
|
7010014c4e | ||
|
|
2c2fc92103 | ||
|
|
37cf9d2591 | ||
|
|
47c5ae215c | ||
|
|
05db26dd93 | ||
|
|
a765cf0a9f | ||
|
|
609a7f0be1 | ||
|
|
fab749e3bd | ||
|
|
d6031a84ae | ||
|
|
aaeb577ffd | ||
|
|
bb1b1e6673 | ||
|
|
9433eb2abb | ||
|
|
fa4b78720d | ||
|
|
1a833a627b | ||
|
|
2b8ded490d | ||
|
|
10d1fefc07 | ||
|
|
fc0e4b51d6 | ||
|
|
8eb75e476d | ||
|
|
4e3119dd9d | ||
|
|
aff75fbd7d | ||
|
|
f568255119 | ||
|
|
283f390e70 | ||
|
|
03f838c5d2 | ||
|
|
2bb8dfdde2 | ||
|
|
2d5be46161 | ||
|
|
396584b5d7 | ||
|
|
0f5bfd3466 | ||
|
|
c963261ee3 | ||
|
|
2a37467d0c | ||
|
|
b20abbbf93 | ||
|
|
a30b0655b9 | ||
|
|
84d331322a | ||
|
|
28c360d708 | ||
|
|
a5f7cd297c | ||
|
|
19906c5725 | ||
|
|
b948b4183a | ||
|
|
5ffce68742 | ||
|
|
6a2bf5c108 | ||
|
|
83ef6f5760 | ||
|
|
59de371d9a | ||
|
|
362250868d | ||
|
|
846d4b9ec7 | ||
|
|
6d8f4e0992 | ||
|
|
b41bfbd47e | ||
|
|
d774d5f073 | ||
|
|
6259cfface | ||
|
|
7cd101bb57 | ||
|
|
329bd4696a | ||
|
|
750a12e14b | ||
|
|
f46a8fee5b | ||
|
|
0d8f51bc4f | ||
|
|
d3c01133b7 | ||
|
|
abb7d2211f | ||
|
|
dd7088c332 | ||
|
|
27bbfc6519 | ||
|
|
c1433392d4 | ||
|
|
f7131d361c | ||
|
|
5840889fa9 | ||
|
|
887b57fca7 | ||
|
|
c496912d88 | ||
|
|
442a58c74a | ||
|
|
ac1632c1e1 | ||
|
|
25dc072b2e | ||
|
|
46b575ae6d | ||
|
|
24fafbe4ff | ||
|
|
18f6d6157c | ||
|
|
3c27f19ea1 | ||
|
|
ad7c36f235 | ||
|
|
6caebdab25 | ||
|
|
58423df517 | ||
|
|
a547382f1c | ||
|
|
bf178fb086 | ||
|
|
fc023f09cb | ||
|
|
3fe9683c98 | ||
|
|
8d20225a57 | ||
|
|
b83c90dca1 | ||
|
|
47de8e89c3 | ||
|
|
64606c2a98 | ||
|
|
b5e913d85e | ||
|
|
8e5dcb3e78 | ||
|
|
ebd03acff0 | ||
|
|
0fe06bfd12 | ||
|
|
3f564ab66a | ||
|
|
9e6349739a | ||
|
|
d1936c6358 | ||
|
|
a075df412c | ||
|
|
b53d84f706 | ||
|
|
0312174e40 | ||
|
|
442ced9124 | ||
|
|
ea9345d82c | ||
|
|
132bf930fb | ||
|
|
980fbef339 | ||
|
|
c68e9abfd4 | ||
|
|
530435a069 | ||
|
|
8cbd4fd957 | ||
|
|
3152e5a76d |
@@ -0,0 +1,82 @@
|
||||
version: '2'
|
||||
|
||||
checks:
|
||||
argument-count:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: 4
|
||||
complex-logic:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: 4
|
||||
file-lines:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: 250
|
||||
method-complexity:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: 10
|
||||
method-count:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: 20
|
||||
method-lines:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: 25
|
||||
nested-control-flow:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: 4
|
||||
return-statements:
|
||||
enabled: false
|
||||
similar-code:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: # Language-specific defaults. Overrides affect all languages.
|
||||
identical-code:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: # Language-specific defaults. Overrides affect all languages.
|
||||
|
||||
exclude_patterns:
|
||||
- '.github/'
|
||||
- '.husky/'
|
||||
- '.vscode/'
|
||||
- 'adminer-plugins/'
|
||||
- 'anonyymioodi/'
|
||||
- 'cypress/'
|
||||
- 'documentation/'
|
||||
- 'scripts/'
|
||||
- 'services/backend/src/database/migrations_kone/'
|
||||
- 'services/backend/src/database/migrations_user/'
|
||||
- 'services/backend/src/models/'
|
||||
- 'updater/sis-updater-worker/src/db/migrations/'
|
||||
- 'updater/sis-updater-worker/src/db/models/'
|
||||
- '.codeclimate.yml'
|
||||
- '.env.template'
|
||||
- '.lintstagedrc.js'
|
||||
- '.nycrc'
|
||||
- '.prettierrc'
|
||||
- '.stylelintrc'
|
||||
- 'cypress.config.js'
|
||||
- 'LICENSE'
|
||||
- 'nginx.conf'
|
||||
- 'nyc-config.js'
|
||||
- '**/.dockerignore'
|
||||
- '**/.eslintrc.json'
|
||||
- '**/.gitignore'
|
||||
- '**/dev.Dockerfile'
|
||||
- '**/Dockerfile'
|
||||
- '**/*.css'
|
||||
- '**/*.csv'
|
||||
- '**/*.html'
|
||||
- '**/*.json'
|
||||
- '**/*.md'
|
||||
- '**/*.png'
|
||||
- '**/*.sh'
|
||||
- '**/*.sql'
|
||||
- '**/*.svg'
|
||||
- '**/*.txt'
|
||||
- '**/*.yml'
|
||||
@@ -15,13 +15,70 @@
|
||||
"plugin:import/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.ts"],
|
||||
"env": {
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"airbnb-typescript",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2021,
|
||||
"sourceType": "module",
|
||||
"project": true
|
||||
},
|
||||
"rules": {
|
||||
"import/prefer-default-export": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/return-await": "off",
|
||||
"@typescript-eslint/no-shadow": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"rules": {
|
||||
"camelcase": "off",
|
||||
"consistent-return": "off",
|
||||
"cypress/no-assigning-return-values": "off",
|
||||
"cypress/unsafe-to-chain-command": "off",
|
||||
"default-param-last": "off",
|
||||
"import/order": "off",
|
||||
"import/extensions": ["error", "never"],
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"alphabetize": { "order": "asc", "caseInsensitive": true },
|
||||
"groups": [["builtin", "external"], ["internal"], ["parent"], ["sibling", "index"]],
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "../../../**",
|
||||
"group": "internal",
|
||||
"position": "before"
|
||||
},
|
||||
{
|
||||
"pattern": "../../**",
|
||||
"group": "internal",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "../**",
|
||||
"group": "parent",
|
||||
"position": "before"
|
||||
},
|
||||
{
|
||||
"pattern": "./**",
|
||||
"group": "sibling",
|
||||
"position": "before"
|
||||
}
|
||||
],
|
||||
"pathGroupsExcludedImportTypes": ["builtin", "external"]
|
||||
}
|
||||
],
|
||||
"no-async-promise-executor": "off",
|
||||
"no-await-in-loop": "off",
|
||||
"no-console": "error",
|
||||
@@ -43,5 +100,12 @@
|
||||
"prettier/prettier": "error",
|
||||
"quotes": ["error", "single", { "avoidEscape": true }]
|
||||
},
|
||||
"ignorePatterns": ["node_modules/", "build/", "**/*migrations*/**"]
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"extensions": [".js", ".jsx", ".ts", ".tsx"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"ignorePatterns": ["build/", "dist/", "instrumented/", "node_modules/", "**/*migrations*/**"]
|
||||
}
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: '03:00'
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'infra'
|
||||
allow:
|
||||
- dependency-type: 'production'
|
||||
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'
|
||||
allow:
|
||||
- dependency-type: 'production'
|
||||
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'
|
||||
allow:
|
||||
- dependency-type: 'production'
|
||||
ignore:
|
||||
- dependency-name: '*'
|
||||
update-types: ['version-update:semver-major']
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: '/services/backend'
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: '03:00'
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'back'
|
||||
allow:
|
||||
- dependency-type: 'production'
|
||||
ignore:
|
||||
- dependency-name: '*'
|
||||
update-types: ['version-update:semver-major']
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: '/services/frontend'
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: '03:00'
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'front'
|
||||
allow:
|
||||
- dependency-type: 'production'
|
||||
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
|
||||
@@ -33,6 +33,7 @@ jobs:
|
||||
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
|
||||
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
|
||||
SENTRY_ENVIRONMENT=production
|
||||
SENTRY_DSN=${{ secrets.OODIKONE_SENTRY_DSN }}
|
||||
|
||||
- name: Push to quay.io
|
||||
uses: redhat-actions/push-to-registry@v2
|
||||
@@ -81,6 +82,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- release_frontend
|
||||
- release_backend
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -89,12 +91,18 @@ jobs:
|
||||
id: extract_version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Pull image and create a container
|
||||
- name: Pull frontend image and create a container
|
||||
run: docker create --name frontend quay.io/toska/oodikone-frontend:production
|
||||
|
||||
- name: Copy assets from the container
|
||||
- name: Pull backend image and create a container
|
||||
run: docker create --name backend quay.io/toska/oodikone-backend:production
|
||||
|
||||
- name: Copy assets from the frontend container
|
||||
run: docker cp frontend:/opt/app-root/src/build ./build
|
||||
|
||||
- name: Copy assets from the backend container
|
||||
run: docker cp backend:/opt/app-root/src/dist ./dist
|
||||
|
||||
- name: Create Sentry release
|
||||
uses: getsentry/action-release@v1
|
||||
env:
|
||||
@@ -105,7 +113,7 @@ jobs:
|
||||
with:
|
||||
environment: production
|
||||
set_commits: 'skip'
|
||||
sourcemaps: './build/assets'
|
||||
sourcemaps: './build/assets ./dist'
|
||||
url_prefix: '~/assets'
|
||||
version: ${{ steps.extract_version.outputs.VERSION }}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- tsMerge
|
||||
paths:
|
||||
- '.github/workflows/test.yaml'
|
||||
- '.github/workflows/staging.yaml'
|
||||
|
||||
@@ -24,8 +24,8 @@ jobs:
|
||||
run: npm ci && cd services/frontend && npm ci && cd ../backend && npm ci
|
||||
- name: 'Run Stylelint for CSS files'
|
||||
run: npm run stylelint "services/frontend/src/**/*.css"
|
||||
- name: 'Run ESLint for JS files'
|
||||
run: npm run eslint -- --ext .jsx,.js services --report-unused-disable-directives
|
||||
- name: 'Run ESLint for JS and TS files'
|
||||
run: npm run eslint -- --ext .js,.jsx,.ts,.tsx services --report-unused-disable-directives
|
||||
|
||||
# === Build database images ===
|
||||
build_backend:
|
||||
|
||||
@@ -16,16 +16,12 @@ pids
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
# Production
|
||||
dist/
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
node_modules/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
@@ -39,10 +35,29 @@ jspm_packages/
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# dotenv environment variables file
|
||||
# Misc
|
||||
*.sqz
|
||||
|
||||
# Environment variable files
|
||||
.env
|
||||
.env.test
|
||||
.env.production
|
||||
|
||||
# Oodikone related stuff
|
||||
.databasedumps
|
||||
.databasedumps/
|
||||
|
||||
# Cypress
|
||||
cypress/downloads/
|
||||
cypress/screenshots/
|
||||
cypress/videos/
|
||||
|
||||
# Code coverage
|
||||
.nyc_output/
|
||||
coverage/
|
||||
instrumented/
|
||||
|
||||
# pycharm
|
||||
.idea
|
||||
|
||||
# fd specific
|
||||
environments/
|
||||
@@ -1 +0,0 @@
|
||||
_
|
||||
@@ -4,8 +4,9 @@ const dockerCmdBase = `docker run --rm --volume ${cwd}:/oodikone --workdir /oodi
|
||||
const relativeFilePaths = files => [...files.map(file => path.relative(cwd, file))].join(' ')
|
||||
|
||||
module.exports = {
|
||||
'{services,updater}/**/*.{js,jsx}': files => `eslint --fix ${files.join(' ')} --report-unused-disable-directives`,
|
||||
'*.{js,json,md,yml,yaml,html,css}': files => `prettier --write ${files.join(' ')}`,
|
||||
'{services,updater}/**/*.{js,jsx,ts,tsx}': files =>
|
||||
`eslint --fix ${files.join(' ')} --report-unused-disable-directives`,
|
||||
'*.{js,jsx,ts,tsx,json,md,yml,yaml,html,css}': files => `prettier --write ${files.join(' ')}`,
|
||||
'*.css': files => `stylelint --fix ${files.join(' ')}`,
|
||||
Dockerfile: files =>
|
||||
`${dockerCmdBase} hadolint/hadolint:v2.12.0 hadolint --ignore DL3006 ${relativeFilePaths(files)}`,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"all": true,
|
||||
"include": ["services/frontend/src/**/*.js", "services/frontend/src/**/*.jsx"],
|
||||
"extends": "./nyc-config.js"
|
||||
}
|
||||
@@ -6,21 +6,23 @@ An application for analyzing university data, running at [https://oodikone.helsi
|
||||
|
||||
To run Oodikone locally, you will need the following:
|
||||
|
||||
1. Applications:
|
||||
- [Docker](https://docs.docker.com/install/) (version 20.10+),
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) (version 1.29+) and
|
||||
- [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.
|
||||
4. For some features to work, access to Toskas service tokens
|
||||
1. Applications
|
||||
- [Docker](https://docs.docker.com/install/) (version `20.10+`),
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) (version `1.29+`)
|
||||
- [npm](https://docs.npmjs.com/cli/v7) (version `7+`)
|
||||
2. Access to Toska Docker Hub
|
||||
3. For real data setup, access to Toska's production servers
|
||||
4. For some features to work, access to Toska's service tokens
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
Pre-installation steps:
|
||||
|
||||
- Clone this repo
|
||||
- Login to toskas docker hub (see Toskas Gitlab for more information)
|
||||
- Populate `.env.template` file with correct tokens and rename it to `.env`
|
||||
- `git@github.com:UniversityOfHelsinkiCS/oodikone.git`
|
||||
- Login to Toska's Docker Hub (see Toska's GitLab for more information)
|
||||
- Copy the `.env.template` file to `.env` and populate with correct tokens
|
||||
- `cp .env.template .env`
|
||||
|
||||
Then launch the CLI and follow the instructions:
|
||||
|
||||
@@ -32,23 +34,26 @@ _Please use a terminal at least 80 characters wide, the CLI is a bit rudimentary
|
||||
|
||||
What different CLI options do:
|
||||
|
||||
1. Set up oodikone from scratch:
|
||||
1. Set up Oodikone from scratch
|
||||
- Cleans up any previous installations
|
||||
- Installs all needed npm packages locally
|
||||
- Sets up pre-commit linting hooks.
|
||||
- Pulls and builds all needed Docker images and sets up dockerized development environment.
|
||||
- Setup happens with anonymized data, which developers should use by default.
|
||||
- **Note:** Running this option cleans up all real data too, so please don't run option 2 before this.
|
||||
2. Reset all real data:
|
||||
- Sets up pre-commit linting hooks
|
||||
- Pulls and builds all needed Docker images and sets up dockerized development environment
|
||||
- Setup happens with anonymized data, which developers should use by default
|
||||
- **Note:** Running this option cleans up all real data too, so please don' run option 2 before this.
|
||||
2. Reset all real data
|
||||
- Cleans up any previous real data databases
|
||||
- Downloads needed database dumps from production servers.
|
||||
- Creates real data databases and populates them with downloaded dumps.
|
||||
3. Reset single database
|
||||
- Like option 2, but only for single database. Leaves other databases untouched.
|
||||
3. Reset a single database
|
||||
- Like option 2, but only for single database
|
||||
- Leaves other databases untouched
|
||||
4. Restore data from dumps
|
||||
- Like option 2, but skips downloading, and simply restores databases from the dumps downloaded earlier.
|
||||
- Like option 2, but skips downloading, and simply restores databases from the dumps downloaded earlier
|
||||
5. Docker system prune
|
||||
- Runs `docker system prune -a && docker volume prune -a`. This will remove all your docker data, also related to any other applications than oodikone! It is sometimes necessary after strange errors due to caching.
|
||||
- Runs `docker system prune -a && docker volume prune -a`
|
||||
- This will remove all your Docker data, also related to any other applications than Oodikone!
|
||||
- Sometimes necessary after strange errors due to caching
|
||||
|
||||
## ⌨️ Development
|
||||
|
||||
@@ -94,9 +99,9 @@ graph TB
|
||||
end
|
||||
```
|
||||
|
||||
In development, backend and updater use the same redis, but in production they are separate.
|
||||
In development, backend and updater use the same Redis, but in production they are separate.
|
||||
|
||||
See also [older graph](documentation/Oodikone.png), which presents more information.
|
||||
See also the [older graph](documentation/img/oodikone.png) for more information.
|
||||
|
||||
To read a bit more details about what updater-scheduler and updater-worker do, see their own READMEs:
|
||||
|
||||
@@ -105,53 +110,57 @@ To read a bit more details about what updater-scheduler and updater-worker do, s
|
||||
|
||||
### Documentation
|
||||
|
||||
Folder `documentation` currently contains some notes on sis-db schema and is meant as a place for storing useful information on Oodikone to make life easier for future developers. Feel free to add anything relevant!
|
||||
The `documentation` directory contains some notes on the sis-db schema and is meant as a place for storing useful information on Oodikone to make life easier for future developers. Feel free to add anything relevant!
|
||||
|
||||
For internal meeting notes and sensitive information, see Toska GitLab.
|
||||
|
||||
[SortableTable documentation](./services/frontend/src/components/SortableTable/README.md)
|
||||
|
||||
### 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**.
|
||||
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**.
|
||||
|
||||
Running oodikone with real data requires separated databases and redis, which are defined in `docker-compose.real.yml` file. Otherwise real data development environment uses the configuration as anonymized data development environment.
|
||||
Running Oodikone with real data requires separated databases and redis, which are defined in `docker-compose.real.yml` file. Otherwise real data development environment uses the configuration as anonymized data development environment.
|
||||
|
||||
Some useful commands are defined in `package.json` and can be run with `npm run <command>` as follows:
|
||||
|
||||
- `npm run oodikone`: starts oodikone with anonymized data
|
||||
- `npm run oodikone:real`: starts oodikone with real data
|
||||
- `npm run oodikone`: starts Oodikone with anonymized data
|
||||
- `npm run oodikone:real`: starts Oodikone with real data
|
||||
- `npm run updater`: starts updater with anonymized data
|
||||
- `npm run updater:real`: starts oodikone with real data
|
||||
- `npm run both`: starts oodikone and updater with anonymized data
|
||||
- `npm run both:real`: starts oodikone and updater with real data
|
||||
- `npm run updater:real`: starts updater with real data
|
||||
- `npm run both`: starts Oodikone and updater with anonymized data
|
||||
- `npm run both:real`: starts Oodikone and updater with real data
|
||||
- `npm run docker:down`: stops the whole environment
|
||||
- `npm run testupdater`: runs tests for updater
|
||||
- `npm run flushredis`: clears redis, which forces big calculations to be redone in e.g. studyprogramme overview and faculty views.
|
||||
- `npm run flushredis`: clears Redis, which forces big calculations to be redone in e.g. studyprogramme overview and faculty views
|
||||
- `npm run rapodiff`: runs [Rapodiff](./services/backend/src/rapodiff/)
|
||||
- `npm run install:local`: installs `node_modules` for each package (backend, frontend, sis-updater-scheduler and sis-updater-worker) locally
|
||||
- `npm run install:docker`: installs `node_modules` for each package inside containers
|
||||
- `npm run install:both`: runs both `npm run install:local` and `npm run install:docker`
|
||||
|
||||
Once you have ran setup for oodikone , you can just execute the first one (`npm run oodikone`). After starting and waiting for a while for containers to compile, oodikone can be accessed at [http://localhost:3000/](http://localhost:3000/) and Adminer (database investigation tool) at [http://localhost:5050/](http://localhost:5050/). Adminer requires you to login with username `postgres` and with any password you choose (for example `p`).
|
||||
Once you have ran setup for Oodikone , you can just execute the first one (`npm run oodikone`). After starting and waiting for a while for containers to compile, Oodikone can be accessed at [http://localhost:3000](http://localhost:3000/) and Adminer (database investigation tool) at [http://localhost:5050](http://localhost:5050/). Adminer requires you to login with username `postgres` and with any password you choose (for example `p`).
|
||||
|
||||
### Run.sh script
|
||||
|
||||
As said, the development environment runs entirely inside docker containers. To keep `package.json` clean and not filled with predefined scripts, we have created a simple helper script called `./run.sh`. The script allows you to use docker-compose commands without the need to write long list of parameters. Try to run `./run.sh` in the root of the project and see what happens!
|
||||
As said, the development environment runs entirely inside Docker containers. To keep `package.json` clean and not filled with predefined scripts, we have created a simple helper script called `./run.sh`. The script allows you to use docker-compose commands without the need to write long list of parameters. Try to run `./run.sh` in the root of the project and see what happens!
|
||||
|
||||
`./run.sh` is simply a wrapper script to run oodikone, updater or both services in either anon or real mode. If you take a look at `package.json`, you can see that most of the predefined scripts above use `./run.sh` under the hood.
|
||||
`./run.sh` is simply a wrapper script to run Oodikone, updater or both services in either anon or real mode. If you take a look at `package.json`, you can see that most of the predefined scripts above use `./run.sh` under the hood.
|
||||
|
||||
It is recommended to spend some time to become familiar with `docker` and `docker compose` cli commands. You can then use them directly or with `./run.sh` wrapper. Here is some examples for day-to-day development situations:
|
||||
|
||||
- `./run.sh oodikone anon pull`: Pull all images related to oodikone development
|
||||
- `./run.sh updater real up --build --force-recreate --detach`: Start updater detached (=in the background) in real data mode, but build new images before starting
|
||||
- `./run.sh oodikone anon pull`: pull all images related to Oodikone development
|
||||
- `./run.sh updater real up --build --force-recreate --detach`: start updater detached (in the background) in real data mode, but build new images before starting
|
||||
- `docker compose ps`: view the containers in the running environment
|
||||
- `docker compose logs frontend`: print logs for just frontend
|
||||
- `docker compose logs --follow --tail 100 backend`: print last hundred rows of backend logs and begin to follow them in your terminal window
|
||||
- `docker exec -it backend sh`: open bash terminal inside backend container
|
||||
- `docker exec -it sis-db psql -U postgres sis-db-real`: open psql client to investigate sis real data database.
|
||||
- `docker exec -it sis-db psql -U postgres sis-db-real`: open psql client to investigate sis real data database
|
||||
|
||||
### User types for development
|
||||
|
||||
By default, you're login as `mluukkai` dev user when running oodikone in development mode. If you want to debug with certain type of user, you can use mocking: go to "Users" -page, click on "edit" on user you want to use and then click icon on the right corner of user card. Another way is to mock user headers - see frontend's api configuration for how this is done.
|
||||
By default, you're login as `mluukkai` dev user when running Oodikone in development mode. If you want to debug with certain type of user, you can use mocking: go to the "Users" page, click "edit" on the user you want to use and then click the icon on the right corner of user card. Another way is to mock user headers - see frontend's API configuration for how this is done.
|
||||
|
||||
Anon data contains some preset user types, corresponding to most usual user types in real data. These are used in testing too (see "Testing" below for more info).
|
||||
Anonymous data contains some preset user types, corresponding to most usual user types in real data. These are used in testing too (see "Testing" below for more info).
|
||||
|
||||
### Linting
|
||||
|
||||
@@ -177,8 +186,8 @@ Continuous integration (CI) works with Github actions and is defined in workflow
|
||||
|
||||
- Oodikone setup for cypress and other tests in CI is defined in `docker-compose.ci.yml`. Take a look at this too if debugging github action workflows.
|
||||
- Tests are run on every push
|
||||
- After a successful test run, oodikone is deployed to staging
|
||||
- After creating a release, oodikone is deployed to production
|
||||
- After a successful test run, Oodikone is deployed to staging
|
||||
- After creating a release, Oodikone is deployed to production
|
||||
- Updater is deployed to production when pushed if tests pass
|
||||
- Test databases can be updated, instructions in [anonyymioodi README](anonyymioodi/README.md)
|
||||
|
||||
@@ -198,14 +207,12 @@ Make sure you have your VPN on.
|
||||
|
||||
First: Try `npm run cli` option 1: _Set up oodikone from scratch_ and then option 2 to download and restore databases (or option 4 to skip downloading, if you've done it recently).
|
||||
|
||||
If that does not help, try option 5: _Docker system prune_. Notice that this clears up **all docker data**, including data related to other projects. If this is not ok, you'll need to identify the oodikone-specific docker volumes yourself and caches and clear those invididually.
|
||||
If that does not help, try option 5: _Docker system prune_. Notice that this clears up **all Docker data**, including data related to other projects. If this is not ok, you'll need to identify the Oodikone-specific Docker volumes yourself and caches and clear those invididually.
|
||||
|
||||
## ✌🏼 Maintainers and contribution
|
||||
|
||||
[Toska](https://toska.dev) of course.
|
||||
[Toska](https://toska.dev) - University of Helsinki
|
||||
|
||||
University of Helsinki
|
||||
## 🛡️ Tietosuoja / data protection
|
||||
|
||||
## Tietosuoja / data protection
|
||||
|
||||
See [tietosuoja.md](tietosuoja.md).
|
||||
See [tietosuoja.md](./documentation/tietosuoja.md).
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
*.sqz
|
||||
|
||||
@@ -9,7 +9,7 @@ services:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: kone-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: postgres:15.3
|
||||
image: postgres:15.7
|
||||
profiles:
|
||||
- oodikone
|
||||
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: sis-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: postgres:15.3
|
||||
image: postgres:15.7
|
||||
shm_size: 2gb
|
||||
|
||||
sis-importer-db:
|
||||
@@ -30,7 +30,7 @@ services:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: sis-importer-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: postgres:15.3
|
||||
image: postgres:15.7
|
||||
profiles:
|
||||
- updater
|
||||
|
||||
@@ -41,6 +41,6 @@ services:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: user-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: postgres:15.3
|
||||
image: postgres:15.7
|
||||
profiles:
|
||||
- oodikone
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/* eslint-disable global-require */
|
||||
/* eslint-disable import/extensions */
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
|
||||
const { defineConfig } = require('cypress')
|
||||
|
||||
module.exports = defineConfig({
|
||||
@@ -11,7 +15,9 @@ module.exports = defineConfig({
|
||||
// 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)
|
||||
require('@cypress/code-coverage/task')(on, config)
|
||||
require('./cypress/plugins/index.js')(on, config)
|
||||
return config
|
||||
},
|
||||
baseUrl: 'http://localhost:3000',
|
||||
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
screenshots
|
||||
videos
|
||||
@@ -4,6 +4,13 @@ const openCompletedCoursesModal = () => {
|
||||
cy.get('button').contains('Search completed courses of students').click()
|
||||
}
|
||||
|
||||
const hasLanded = () => {
|
||||
cy.contains('Search completed courses')
|
||||
cy.contains(
|
||||
'Here you can search by a list of student and course numbers to see whether students have completed certain courses yet'
|
||||
)
|
||||
}
|
||||
|
||||
const openSearch = () => {
|
||||
cy.init('/completedcoursessearch')
|
||||
hasLanded()
|
||||
@@ -12,13 +19,6 @@ const selectSavedCourselist = name => {
|
||||
cy.get('[data-cy="history-search"]').children().eq(0).type(name).type('{enter}')
|
||||
}
|
||||
|
||||
const hasLanded = () => {
|
||||
cy.contains('Search completed courses')
|
||||
cy.contains(
|
||||
'Here you can search by a list of student and course numbers to see whether students have completed certain courses yet'
|
||||
)
|
||||
}
|
||||
|
||||
const deleteAllSearches = () => {
|
||||
cy.contains('Saved courselists')
|
||||
.get('.dropdown')
|
||||
@@ -143,11 +143,11 @@ describe('Courselist saving-related functions work', () => {
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
selectSavedCourselist(courseListNames[0])
|
||||
cy.get('[data-cy="course-list-input"]').type(`,TKT10001`)
|
||||
cy.get('[data-cy="course-list-input"]').type(',TKT10001')
|
||||
cy.get('button').contains('Save').click()
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
selectSavedCourselist(courseListNames[0])
|
||||
cy.get('[data-cy="course-list-input"]').contains(`TKT10001`)
|
||||
cy.get('[data-cy="course-list-input"]').contains('TKT10001')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
const openCustomPopupForm = () => {
|
||||
cy.get('button').contains('Custom population').click()
|
||||
cy.get('[data-cy="custom-pop-search-button"]').click()
|
||||
}
|
||||
|
||||
const fillName = () => {
|
||||
const name = `TEST-${new Date().getTime()}`
|
||||
cy.contains('Insert name for this custom population if you wish to save it')
|
||||
.siblings()
|
||||
.get('input[placeholder=name]')
|
||||
.type(name)
|
||||
cy.get('[data-cy="custom-population-name-input"]').type(name)
|
||||
return name
|
||||
}
|
||||
|
||||
@@ -40,20 +38,17 @@ const deleteAllSearches = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const fillForm = content => {
|
||||
cy.contains('Insert student numbers you wish to use for population here')
|
||||
.siblings()
|
||||
.get('textarea')
|
||||
.type(content.join('\n'))
|
||||
const fillForm = (content, separator) => {
|
||||
cy.get('[data-cy="student-number-input"]').type(content.join(separator))
|
||||
}
|
||||
|
||||
const search = () => {
|
||||
cy.get('button').contains('Search population').click()
|
||||
cy.get('[data-cy="search-button"]').click()
|
||||
}
|
||||
|
||||
const searchFor = studentnumbers => {
|
||||
const searchFor = (studentnumbers, separator) => {
|
||||
openCustomPopupForm()
|
||||
fillForm(studentnumbers)
|
||||
fillForm(studentnumbers, separator)
|
||||
search()
|
||||
}
|
||||
|
||||
@@ -74,8 +69,19 @@ const containsSpecificStudents = (studentnumbers = []) => {
|
||||
studentnumbers.forEach(s => cy.contains(s))
|
||||
}
|
||||
|
||||
const checkRightsNotification = studentNumbers => {
|
||||
cy.get('[data-cy="rights-notification"]').within(() => {
|
||||
cy.contains(
|
||||
'The following students information could not be displayed. This could be either because they do not exist, or you do not have the right to view their information.'
|
||||
)
|
||||
cy.get('ul').within(() => {
|
||||
studentNumbers.forEach(number => cy.get('li').contains(number))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
describe('Custom population tests', () => {
|
||||
const nonExistentStudentNumbers = ['123', 'X', '-', ' ']
|
||||
const nonExistentStudentNumbers = ['123', 'X', '-']
|
||||
beforeEach(() => {
|
||||
cy.init('/custompopulation')
|
||||
cy.url().should('include', '/custompopulation')
|
||||
@@ -83,7 +89,7 @@ describe('Custom population tests', () => {
|
||||
})
|
||||
|
||||
after(() => {
|
||||
cy.visit(`${Cypress.config().baseUrl}/custompopulation`)
|
||||
cy.visit('/custompopulation')
|
||||
cy.url().should('include', '/custompopulation')
|
||||
cy.contains('Custom population')
|
||||
cy.get('button').contains('Custom population').click()
|
||||
@@ -92,9 +98,8 @@ describe('Custom population tests', () => {
|
||||
|
||||
describe('Custom population searching', () => {
|
||||
it('Finds a proper population', () => {
|
||||
cy.fixture('customPopulations').then(({ studentNumbersForCSStudentsSet1 }) => {
|
||||
const students = studentNumbersForCSStudentsSet1
|
||||
searchFor(students)
|
||||
cy.fixture('customPopulations').then(({ studentNumbersForCSStudentsSet1: students }) => {
|
||||
searchFor(students, '\n')
|
||||
hasLanded()
|
||||
containsAmountOfStudents(students.length)
|
||||
containsSpecificStudents(students)
|
||||
@@ -102,17 +107,18 @@ describe('Custom population tests', () => {
|
||||
})
|
||||
|
||||
it("Doesn't return non-existing students", () => {
|
||||
cy.fixture('customPopulations').then(({ studentNumbersForCSStudentsSet1 }) => {
|
||||
const students = [...studentNumbersForCSStudentsSet1, ...nonExistentStudentNumbers]
|
||||
searchFor(students)
|
||||
cy.fixture('customPopulations').then(({ studentNumbersForCSStudentsSet1: students }) => {
|
||||
searchFor([...students, ...nonExistentStudentNumbers], ' ')
|
||||
hasLanded()
|
||||
containsAmountOfStudents(studentNumbersForCSStudentsSet1.length)
|
||||
containsSpecificStudents(studentNumbersForCSStudentsSet1)
|
||||
containsAmountOfStudents(students.length)
|
||||
containsSpecificStudents(students)
|
||||
checkRightsNotification(nonExistentStudentNumbers)
|
||||
})
|
||||
})
|
||||
|
||||
it("Doesn't find empty custom population", () => {
|
||||
searchFor(nonExistentStudentNumbers)
|
||||
searchFor(nonExistentStudentNumbers, ',')
|
||||
checkRightsNotification(nonExistentStudentNumbers)
|
||||
cy.contains('Credit accumulation').should('not.exist')
|
||||
cy.contains('Programme distribution').should('not.exist')
|
||||
cy.contains('Courses of population').should('not.exist')
|
||||
@@ -120,10 +126,11 @@ describe('Custom population tests', () => {
|
||||
})
|
||||
|
||||
it("Doesn't return students user has no right to", () => {
|
||||
// TODO: check that this doesn't create false positives, since this test goes
|
||||
// through even if following students are not in anon sis-db.
|
||||
// These students exist in the database, but the user doesn't have the right to view them
|
||||
const studentsForEduBachStudents = ['014990067', '013069465', '014853890']
|
||||
searchFor(studentsForEduBachStudents)
|
||||
// Two semicolons on purpose, the page should be able to handle it
|
||||
searchFor(studentsForEduBachStudents, ';;')
|
||||
checkRightsNotification(studentsForEduBachStudents)
|
||||
cy.contains('Credit accumulation').should('not.exist')
|
||||
cy.contains('Programme distribution').should('not.exist')
|
||||
cy.contains('Courses of population').should('not.exist')
|
||||
@@ -133,11 +140,14 @@ describe('Custom population tests', () => {
|
||||
|
||||
describe('Custom population search saving', () => {
|
||||
it('Saves a custom population search', { retries: 2 }, () => {
|
||||
cy.fixture('customPopulations').then(({ studentNumbersForCSStudentsSet1 }) => {
|
||||
const students = studentNumbersForCSStudentsSet1
|
||||
cy.fixture('customPopulations').then(({ studentNumbersForCSStudentsSet1: students }) => {
|
||||
openCustomPopupForm()
|
||||
cy.contains(
|
||||
'Insert student numbers you wish to use for population. Separate each number with a comma, semicolon, space, or newline.'
|
||||
)
|
||||
const name = fillName()
|
||||
fillForm(students)
|
||||
// A comma and a newline on purpose, the page should be able to handle it
|
||||
fillForm(students, ',\n')
|
||||
save()
|
||||
|
||||
// Round 1
|
||||
@@ -148,10 +158,9 @@ describe('Custom population tests', () => {
|
||||
containsSpecificStudents(students)
|
||||
|
||||
// Round 2
|
||||
cy.visit(`${Cypress.config().baseUrl}/custompopulation`)
|
||||
cy.url().should('include', '/custompopulation')
|
||||
cy.visit('/custompopulation')
|
||||
cy.contains('Custom population')
|
||||
cy.get('button').contains('Custom population').click()
|
||||
openCustomPopupForm()
|
||||
selectSavedPopulation(name)
|
||||
search()
|
||||
cy.contains(`Custom population "${name}"`)
|
||||
@@ -161,35 +170,34 @@ describe('Custom population tests', () => {
|
||||
})
|
||||
|
||||
it('Updates a custom population search', () => {
|
||||
cy.fixture('customPopulations').then(({ studentNumbersForCSStudentsSet1, studentNumbersForCSStudentsSet2 }) => {
|
||||
const students1 = studentNumbersForCSStudentsSet1
|
||||
const students2 = studentNumbersForCSStudentsSet2
|
||||
openCustomPopupForm()
|
||||
const name = fillName()
|
||||
fillForm(students1)
|
||||
save()
|
||||
cy.fixture('customPopulations').then(
|
||||
({ studentNumbersForCSStudentsSet1: students1, studentNumbersForCSStudentsSet2: students2 }) => {
|
||||
openCustomPopupForm()
|
||||
const name = fillName()
|
||||
fillForm(students1, ' ')
|
||||
save()
|
||||
|
||||
selectSavedPopulation(name)
|
||||
search()
|
||||
cy.contains(`Custom population "${name}"`)
|
||||
containsAmountOfStudents(students1.length)
|
||||
containsSpecificStudents(students1)
|
||||
cy.contains('Custom population')
|
||||
cy.get('button').contains('Custom population').click()
|
||||
selectSavedPopulation(name)
|
||||
fillForm([...students1, ...students2])
|
||||
save()
|
||||
selectSavedPopulation(name)
|
||||
search()
|
||||
cy.contains(`Custom population "${name}"`)
|
||||
containsAmountOfStudents(students1.length)
|
||||
containsSpecificStudents(students1)
|
||||
cy.contains('Custom population')
|
||||
openCustomPopupForm()
|
||||
selectSavedPopulation(name)
|
||||
fillForm(['\n', ...students2], ', ')
|
||||
save()
|
||||
|
||||
cy.visit(`${Cypress.config().baseUrl}/custompopulation`)
|
||||
cy.url().should('include', '/custompopulation')
|
||||
cy.contains('Custom population')
|
||||
cy.get('button').contains('Custom population').click()
|
||||
selectSavedPopulation(name)
|
||||
search()
|
||||
cy.contains(`Custom population "${name}"`)
|
||||
containsAmountOfStudents(students1.length + students2.length)
|
||||
containsSpecificStudents([...students1, ...students2])
|
||||
})
|
||||
cy.visit('/custompopulation')
|
||||
cy.contains('Custom population')
|
||||
openCustomPopupForm()
|
||||
selectSavedPopulation(name)
|
||||
search()
|
||||
cy.contains(`Custom population "${name}"`)
|
||||
containsAmountOfStudents(students1.length + students2.length)
|
||||
containsSpecificStudents([...students1, ...students2])
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
const path = require('path')
|
||||
|
||||
const timestamp = new Date().toISOString().split('T')[0] // YYYY-MM-DD
|
||||
const downloadsFolder = Cypress.config('downloadsFolder')
|
||||
|
||||
describe('Faculty overview', () => {
|
||||
describe('Faculty can be selected', () => {
|
||||
it('Faculties are listed and one can be chosen', () => {
|
||||
@@ -11,7 +16,7 @@ describe('Faculty overview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Faculty basic information: admin user', () => {
|
||||
describe('Basic information: admin user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties', 'admin')
|
||||
cy.contains('a', 'Eläinlääketieteellinen tiedekunta').click()
|
||||
@@ -27,7 +32,7 @@ describe('Faculty overview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Faculty basic information: basic user', () => {
|
||||
describe('Basic information: basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.contains('a', 'Maatalous-metsätieteellinen tiedekunta').click()
|
||||
@@ -76,6 +81,23 @@ describe('Faculty overview', () => {
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-close-info"]').click()
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('not.exist')
|
||||
})
|
||||
|
||||
it('Data can be exported to Excel files', () => {
|
||||
const sections = [
|
||||
'StudentsOfTheFaculty',
|
||||
'GraduatedOfTheFaculty',
|
||||
'ThesisWritersOfTheFaculty',
|
||||
'CreditsProducedByTheFaculty',
|
||||
]
|
||||
sections.forEach(section => {
|
||||
cy.get(`[data-cy="Graph-${section}"]`).within(() => {
|
||||
cy.get('[aria-label="View chart menu, Chart"]').click()
|
||||
cy.contains('li.highcharts-menu-item', 'Download XLS').click()
|
||||
const downloadedFile = `oodikone_${section}_H80_${timestamp}.xls`
|
||||
cy.readFile(path.join(downloadsFolder, downloadedFile))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Study programme information: users with access', () => {
|
||||
@@ -142,7 +164,7 @@ describe('Faculty overview', () => {
|
||||
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('text.highcharts-axis-title', 'Graduation year')
|
||||
cy.contains('.message', "Click a bar to view that year's programme level breakdown").should('be.visible')
|
||||
})
|
||||
})
|
||||
@@ -168,7 +190,7 @@ describe('Faculty overview', () => {
|
||||
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.get('[aria-label="EDUK, 40."]').trigger('mouseover')
|
||||
cy.contains('Kasvatustieteiden kandiohjelma')
|
||||
cy.contains('KH60_001')
|
||||
})
|
||||
@@ -189,7 +211,7 @@ describe('Faculty overview', () => {
|
||||
cy.get('div[class="programmes-graph"]').should('be.visible')
|
||||
cy.get('div[class="programmes-graph"]').within(() => {
|
||||
cy.contains('EDUM')
|
||||
cy.contains('24 graduated').trigger('mouseover')
|
||||
cy.get('[aria-label="EDUM, 22."]').trigger('mouseover')
|
||||
cy.contains('Kasvatustieteiden maisteriohjelma')
|
||||
cy.contains('MH60_001')
|
||||
})
|
||||
@@ -211,17 +233,23 @@ describe('Faculty overview', () => {
|
||||
|
||||
cy.get('[data-cy="GraduationTimeToggle"]').click()
|
||||
cy.get('[data-cy="Section-master"]').within(() => {
|
||||
cy.contains('16').click()
|
||||
cy.get('[aria-label="2018, 16. On time."]').click()
|
||||
cy.get('div[class="programmes-breakdown-graph"]').should('be.visible')
|
||||
cy.get('div[class="programmes-breakdown-graph"]').within(() => {
|
||||
cy.contains('Year 2018 by start year')
|
||||
cy.contains('EDUM')
|
||||
cy.contains('16').trigger('mouseover')
|
||||
cy.get('[aria-label="EDUM, 3. Max. year overtime."]').trigger('mouseover')
|
||||
cy.contains('Kasvatustieteiden maisteriohjelma')
|
||||
cy.contains('MH60_001')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Data can be exported to an Excel file', () => {
|
||||
cy.get('[data-cy="DownloadButton-AverageGraduationTimes"]').click()
|
||||
const downloadedFile = `oodikone_H60_graduation_times_${timestamp}.xlsx`
|
||||
cy.readFile(path.join(downloadsFolder, downloadedFile))
|
||||
})
|
||||
})
|
||||
|
||||
describe('Faculty Progress and student population tab', () => {
|
||||
|
||||
@@ -162,13 +162,13 @@ describe('Population Statistics', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('Credit filter works', () => {
|
||||
it('Credit filter works', { retries: 2 }, () => {
|
||||
runTestStepWithPreAndPostParts('CreditsEarned', () => {
|
||||
testRangeFilter('CreditsEarned-filter-card', 50, 150, 118)
|
||||
})
|
||||
})
|
||||
|
||||
it('Age filter works', () => {
|
||||
it('Age filter works', { retries: 2 }, () => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
testRangeFilter('Age-filter-card', 20, 40, 47)
|
||||
@@ -346,7 +346,7 @@ describe('Custom Population Statistics', () => {
|
||||
cy.fixture('customPopulations').then(({ studentNumbersForCSStudentsSet1, studentNumbersForCSStudentsSet2 }) => {
|
||||
const students = [...studentNumbersForCSStudentsSet1, ...studentNumbersForCSStudentsSet2]
|
||||
runTestStepWithPreAndPostParts = createRunTestStepWithPreAndPostPartsFunction(students.length)
|
||||
cy.cs('student-no-input').click().type(students.join('\n'))
|
||||
cy.cs('student-number-input').click().type(students.join('\n'))
|
||||
})
|
||||
cy.cs('search-button').click()
|
||||
})
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('When language center is opened', () => {
|
||||
it('"Access denied" is shown', () => {
|
||||
cy.init('/languagecenterview', 'norights')
|
||||
cy.contains('Access denied')
|
||||
cy.contains("You're currently not allowed to see this page.")
|
||||
cy.contains("You don't currently have permission to view this page.")
|
||||
cy.contains('Special populations').click()
|
||||
cy.contains('Language center view').should('not.exist')
|
||||
})
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/// <reference types="Cypress" />
|
||||
// Now "Class statistics" in UI
|
||||
const moment = require('moment')
|
||||
const _ = require('lodash')
|
||||
|
||||
describe('Population Statistics tests', () => {
|
||||
const pathToCSBach2017 =
|
||||
@@ -102,7 +100,9 @@ describe('Population Statistics tests', () => {
|
||||
.then($parentDiv => {
|
||||
if (!$parentDiv.hasClass('active')) cy.contains('Students (170)').click()
|
||||
})
|
||||
cy.get('[data-cy=student-table-tabs]').contains('Tags').click()
|
||||
cy.get('[data-cy=student-table-tabs]').within(() => {
|
||||
cy.contains('Tags').click()
|
||||
})
|
||||
cy.contains('No tags defined. You can define them here.').find('a').click()
|
||||
cy.contains('Tags').click()
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
|
||||
@@ -141,12 +141,8 @@ describe('Population Statistics tests', () => {
|
||||
})
|
||||
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())
|
||||
// Months are capped to goal time of the degree, 36 months is the goal time for bachelor's degree
|
||||
const monthsForLimits = Math.min(36, months)
|
||||
const limits = [1, ..._.range(1, 5).map(p => Math.ceil(monthsForLimits * ((p * 15) / 12))), null]
|
||||
const ranges = _.range(1, limits.length).map(i => _.slice(limits, i - 1, i + 1))
|
||||
const limits = [45, 90, 135, 180, null]
|
||||
const ranges = limits.map((limit, i) => (i === 0 ? [1, limit] : [limits[i - 1], limit]))
|
||||
|
||||
cy.get('.credits-gained-table').should('contain', '(n = 170)')
|
||||
|
||||
@@ -165,7 +161,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)
|
||||
const sum = [...$els].map($el => parseInt($el.innerText, 10)).reduce((a, b) => a + b, 0)
|
||||
expect(sum).to.equal(170)
|
||||
})
|
||||
|
||||
@@ -182,7 +178,6 @@ describe('Population Statistics tests', () => {
|
||||
if (!$parentDiv.hasClass('active')) cy.get("[data-cy='Credit statistics']").click()
|
||||
})
|
||||
cy.contains('Credits gained').click()
|
||||
cy.get('.credits-gained-divider').click()
|
||||
cy.get("[data-cy='credits-gained-table-Avoin väylä']").should('exist')
|
||||
cy.get("[data-cy='credits-gained-table-Yhteispisteet']").should('exist')
|
||||
cy.get("[data-cy='credits-gained-table-Muu']").should('exist')
|
||||
@@ -198,8 +193,9 @@ describe('Population Statistics tests', () => {
|
||||
})
|
||||
cy.get("[data-cy='credit-stats-tab'] > .menu > :nth-child(2)").click()
|
||||
|
||||
cy.get("[data-cy='credit-stats-table-name-header']").should('contain', 'Statistic for n = 170 Students')
|
||||
cy.get("[data-cy='credit-stats-mean']").should('contain', '114.74')
|
||||
cy.get("[data-cy='credit-stats-population-size']").should('contain', 'n = 170')
|
||||
cy.get("[data-cy='credit-stats-average']").should('contain', '114.74')
|
||||
cy.get("[data-cy='credit-stats-median']").should('contain', '109.00')
|
||||
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')
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('Students tests', () => {
|
||||
cy.url().should('include', '/students')
|
||||
cy.get('.prompt').type(student.studentnumber)
|
||||
cy.contains('td a', student.studentnumber).click()
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma (01.08.2017 - 31.07.2025')
|
||||
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')
|
||||
|
||||
@@ -64,6 +64,7 @@ describe('Students tests', () => {
|
||||
cy.get('.prompt').type(student.studentnumber)
|
||||
cy.contains('td a', student.studentnumber).click()
|
||||
cy.contains('Introduction to Machine Learning (DATA11002)')
|
||||
.parent()
|
||||
.siblings()
|
||||
.last()
|
||||
.within(() => {
|
||||
@@ -81,6 +82,14 @@ describe('Students tests', () => {
|
||||
.and('include', 'https://sisu.helsinki.fi/tutor/role/staff/student/hy-hlo-84830887/basic/basic-info')
|
||||
})
|
||||
|
||||
it('Bachelor Honours section is shown', () => {
|
||||
cy.get('.prompt').type(student.studentnumber)
|
||||
cy.contains('td a', student.studentnumber).click()
|
||||
cy.contains('h4', 'Bachelor Honours')
|
||||
cy.contains('.tag.label', 'Not qualified for Honours')
|
||||
cy.contains('.tag.label', 'Has not graduated')
|
||||
})
|
||||
|
||||
it('Searching with bad inputs doesnt yield results', () => {
|
||||
cy.get('.prompt').type('SWAG LITTINEN')
|
||||
cy.contains('Student number').should('not.exist')
|
||||
@@ -92,7 +101,9 @@ describe('Students tests', () => {
|
||||
it('Can jump to population page', () => {
|
||||
cy.get('.prompt').type(student.studentnumber)
|
||||
cy.contains('td a', student.studentnumber).click()
|
||||
cy.get('i.level.up.alternate.icon').eq(0).click()
|
||||
cy.contains('.ui.table', 'Completed').within(() => {
|
||||
cy.get('i.level.up.alternate.icon').click()
|
||||
})
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
|
||||
cy.contains('class size')
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('Study guidance group tests', () => {
|
||||
cy.init('/studyguidancegroups')
|
||||
cy.contains('Access denied')
|
||||
cy.contains(
|
||||
"You're currently not allowed to see this page. Please contant oodikone@helsinki.fi, if this is a mistake."
|
||||
"You don't currently have permission to view this page. If you believe this is a mistake, please contact oodikone@helsinki.fi."
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -79,14 +79,7 @@ describe('Study guidance group tests', () => {
|
||||
})
|
||||
|
||||
it('general tab of the students table has the correct columns', () => {
|
||||
const firstRowHeadings = [
|
||||
'Phone number',
|
||||
'Student number',
|
||||
'Credits',
|
||||
'Start year at uni',
|
||||
'Study programmes',
|
||||
'Tags',
|
||||
]
|
||||
const firstRowHeadings = ['Student number', 'Credits', 'Start year at uni', 'Study programmes', 'Tags']
|
||||
const secondRowHeadings = ['All', 'Since 1.1.1970']
|
||||
cy.cs('Students (2)').click()
|
||||
cy.get('[data-cy="student-table-tabs"] table thead tr').then($tr => {
|
||||
@@ -137,17 +130,14 @@ describe('Study guidance group tests', () => {
|
||||
|
||||
it('general tab of the students table has the correct columns', () => {
|
||||
const firstRowHeadings = [
|
||||
'Phone number',
|
||||
'Student number',
|
||||
'Credits',
|
||||
'Start of studyright',
|
||||
'Started in programme',
|
||||
'Semesters present',
|
||||
'Enrollment status',
|
||||
'Graduation date',
|
||||
'Start year at uni',
|
||||
'Other programmes',
|
||||
'Semesters present amount',
|
||||
'Transferred from',
|
||||
'Gender',
|
||||
'Citizenship',
|
||||
|
||||
@@ -23,7 +23,7 @@ const getEmptyYears = isAcademicYear => {
|
||||
}
|
||||
|
||||
describe('Studyprogramme overview', () => {
|
||||
/* Basic information overview -tests*/
|
||||
/* Basic information overview -tests */
|
||||
describe('Basic information -view works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
@@ -42,13 +42,13 @@ describe('Studyprogramme overview', () => {
|
||||
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, 10, 35, 2, 2],
|
||||
[2019, 86, 17, 1, 1],
|
||||
[2018, 161, 1, 0, 0],
|
||||
[2017, 171, 0, 0, 0],
|
||||
// [Year, Started studying, Accepted, Graduated, Transferred Away, Transferred to]
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0]),
|
||||
[2021, 0, 0, 0, 1, 0],
|
||||
[2020, 10, 10, 35, 2, 2],
|
||||
[2019, 83, 86, 17, 1, 1],
|
||||
[2018, 150, 161, 1, 0, 0],
|
||||
[2017, 158, 171, 0, 0, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudentsOfTheStudyprogramme')
|
||||
@@ -62,7 +62,7 @@ describe('Studyprogramme overview', () => {
|
||||
[2020, 4511, 4511, 0, 0, 406],
|
||||
[2019, 7117, 7097, 0, 0, 434],
|
||||
[2018, 5742, 5737, 0, 0, 150],
|
||||
[2017, 1882, 1882, 0, 0, 186],
|
||||
[2017, 1887, 1882, 0, 0, 186],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
@@ -87,13 +87,13 @@ describe('Studyprogramme overview', () => {
|
||||
cy.get('[data-cy=StudentToggle]').click()
|
||||
const years = getEmptyYears()
|
||||
const studentTableContents = [
|
||||
// [Year, Started, Graduated]
|
||||
...years.map(year => [year, 0, 0]),
|
||||
[2021, 0, 0],
|
||||
[2020, 10, 35],
|
||||
[2019, 86, 17],
|
||||
[2018, 161, 1],
|
||||
[2017, 171, 0],
|
||||
// [Year, Started studying, Accepted, Graduated]
|
||||
...years.map(year => [year, 0, 0, 0]),
|
||||
[2021, 0, 0, 0],
|
||||
[2020, 10, 10, 35],
|
||||
[2019, 83, 86, 17],
|
||||
[2018, 150, 161, 1],
|
||||
[2017, 158, 171, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
|
||||
@@ -104,7 +104,7 @@ describe('Studyprogramme overview', () => {
|
||||
[2020, 4511, 4511, 0, 0, 406],
|
||||
[2019, 7117, 7097, 0, 0, 434],
|
||||
[2018, 5742, 5737, 0, 0, 150],
|
||||
[2017, 1882, 1882, 0, 0, 186],
|
||||
[2017, 1887, 1882, 0, 0, 186],
|
||||
]
|
||||
|
||||
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
@@ -128,13 +128,13 @@ describe('Studyprogramme overview', () => {
|
||||
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', 10, 6, 3, 2],
|
||||
['2019 - 2020', 86, 38, 1, 0],
|
||||
['2018 - 2019', 161, 8, 0, 1],
|
||||
['2017 - 2018', 171, 1, 0, 0],
|
||||
// [Year, Started studying, Accepted, Graduated, Transferred away, Transferred to]
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0]),
|
||||
['2021 - 2022', 0, 0, 0, 0, 0],
|
||||
['2020 - 2021', 10, 10, 6, 1, 2],
|
||||
['2019 - 2020', 83, 86, 38, 3, 0],
|
||||
['2018 - 2019', 150, 161, 8, 0, 1],
|
||||
['2017 - 2018', 158, 171, 1, 0, 0],
|
||||
]
|
||||
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
|
||||
|
||||
@@ -144,7 +144,7 @@ describe('Studyprogramme overview', () => {
|
||||
['2020 - 2021', 352, 352, 0, 0, 296],
|
||||
['2019 - 2020', 6966, 6966, 0, 0, 430],
|
||||
['2018 - 2019', 7797, 7777, 0, 0, 259],
|
||||
['2017 - 2018', 4137, 4132, 0, 0, 191],
|
||||
['2017 - 2018', 4142, 4132, 0, 0, 191],
|
||||
]
|
||||
|
||||
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
@@ -153,9 +153,11 @@ describe('Studyprogramme overview', () => {
|
||||
|
||||
it('Basic information graphs render', () => {
|
||||
cy.get('[data-cy=Graph-StudentsOfTheStudyprogramme')
|
||||
.should('contain', 'Started')
|
||||
.should('contain', 'Started studying')
|
||||
.should('contain', 'Accepted')
|
||||
.should('contain', 'Graduated')
|
||||
.should('contain', 'Transferred away')
|
||||
.should('contain', 'Transferred to')
|
||||
|
||||
cy.get('[data-cy=Graph-CreditsProducedByTheStudyprogramme')
|
||||
.should('contain', 'Degree students')
|
||||
@@ -176,11 +178,11 @@ describe('Studyprogramme overview', () => {
|
||||
cy.get('[data-cy=GraduationTimeToggle]').click()
|
||||
cy.get('[data-cy=graduation-times-graphBachelor]').within(() => {
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2019')
|
||||
cy.contains('17 graduated').trigger('mouseover')
|
||||
cy.contains('study time: 23 months')
|
||||
cy.contains('17 graduated on time')
|
||||
cy.contains('0 graduated max year overtime')
|
||||
cy.contains('2020')
|
||||
cy.contains('35 graduated').trigger('mouseover')
|
||||
cy.contains('median study time: 34 months')
|
||||
cy.contains('30 graduated on time')
|
||||
cy.contains('5 graduated max year overtime')
|
||||
})
|
||||
|
||||
cy.get('[data-cy=Graph-ProgrammesBeforeOrAfter')
|
||||
@@ -206,9 +208,9 @@ describe('Studyprogramme overview', () => {
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2020')
|
||||
cy.contains('24 graduated').trigger('mouseover')
|
||||
cy.contains('study time: 22 months')
|
||||
cy.contains('20 graduated on time')
|
||||
cy.contains('4 graduated max year overtime')
|
||||
cy.contains('study time: 22.5 months')
|
||||
cy.contains('19 graduated on time')
|
||||
cy.contains('5 graduated max year overtime')
|
||||
})
|
||||
|
||||
cy.get('[data-cy=graduation-times-graphBachelor]').within(() => {
|
||||
@@ -221,7 +223,7 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
/* Studytrack overview -tests*/
|
||||
/* Studytrack overview -tests */
|
||||
describe('Studytrack overview works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
@@ -238,27 +240,28 @@ describe('Studyprogramme overview', () => {
|
||||
|
||||
it('Students of the studyprogramme are shown correctly', () => {
|
||||
const tableContents = [
|
||||
// [Year, All, Started studying, Currently enrolled, Absent, Inactive, Graduated, Men, Women, other/unknown Finnish, other]
|
||||
// [Year, All, Started studying, Present, Absent, Inactive, Graduated, Men, Women, Other/Unknown, Finland, Other]
|
||||
['2020 - 2021', 12, 10, 0, 0, 12, 0, 10, 2, 0, 12, 0],
|
||||
['2019 - 2020', 86, 86, 0, 0, 85, 1, 65, 21, 0, 86, 0],
|
||||
['2018 - 2019', 162, 161, 0, 0, 151, 11, 118, 44, 0, 161, 1],
|
||||
['2017 - 2018', 171, 171, 0, 0, 129, 42, 135, 36, 0, 166, 5],
|
||||
['Total', 431, 428, 0, 0, 377, 54, 328, 103, 0, 425, 6],
|
||||
['2019 - 2020', 86, 83, 0, 0, 85, 1, 65, 21, 0, 86, 0],
|
||||
['2018 - 2019', 162, 150, 0, 0, 151, 11, 118, 44, 0, 161, 1],
|
||||
['2017 - 2018', 171, 158, 0, 0, 130, 41, 135, 36, 0, 166, 5],
|
||||
['Total', 431, 401, 0, 0, 378, 53, 328, 103, 0, 425, 6],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudytrackOverview')
|
||||
})
|
||||
|
||||
it('Student progress data is shown correctly', () => {
|
||||
const years = getEmptyYears(true)
|
||||
const tableContents = [
|
||||
['2023 - 2024', 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
['2022 - 2023', 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
// [Year, All, < 30 credits, 30–60 credits, 60–90 credits, 90–120 credits, 120–150 credits, 150–180 credits, ≥ 180 credits]
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0]),
|
||||
['2021 - 2022', 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
['2020 - 2021', 12, 12, 0, 0, 0, 0, 0, 0],
|
||||
['2019 - 2020', 86, 26, 34, 21, 5, 0, 0, 0],
|
||||
['2018 - 2019', 162, 10, 27, 47, 36, 26, 12, 4],
|
||||
['2018 - 2019', 162, 10, 28, 47, 35, 26, 12, 4],
|
||||
['2017 - 2018', 171, 20, 21, 26, 22, 22, 33, 27],
|
||||
['Total', 431, 68, 82, 94, 63, 48, 45, 31],
|
||||
['Total', 431, 68, 83, 94, 62, 48, 45, 31],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudytrackProgress')
|
||||
@@ -276,15 +279,15 @@ describe('Studyprogramme overview', () => {
|
||||
|
||||
cy.get('[data-cy=graduation-times-graphBachelor]').within(() => {
|
||||
cy.contains('Start year')
|
||||
cy.contains('2019 - 2020')
|
||||
cy.contains('42 graduated').trigger('mouseover')
|
||||
cy.contains('study time: 31.5 months')
|
||||
cy.contains('39 graduated on time')
|
||||
cy.contains('2017 - 2018')
|
||||
cy.contains('41 graduated').trigger('mouseover')
|
||||
cy.contains('study time: 31 months')
|
||||
cy.contains('36 graduated on time')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/* Programme courses -tests*/
|
||||
/* Programme courses -tests */
|
||||
describe('Programme courses works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
@@ -397,7 +400,9 @@ describe('Studyprogramme overview', () => {
|
||||
cy.get('.form > .field > .dropdown > .visible').contains(name).click()
|
||||
|
||||
cy.get('textarea').type('010113437')
|
||||
cy.get('.positive').click()
|
||||
cy.contains('Add tags').click()
|
||||
|
||||
cy.contains('Successfully added tags to students.')
|
||||
|
||||
cy.contains('Students').click()
|
||||
cy.get('.prompt').type(student)
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('Teachers page tests', () => {
|
||||
|
||||
it('Check Statistics', () => {
|
||||
cy.get(':nth-child(1) > .ui > .search').click()
|
||||
cy.contains('Autumn 2020').click()
|
||||
cy.contains('Syksy 2020').click()
|
||||
cy.cs('course-providers').click()
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.cs('course-providers').children('.icon').click()
|
||||
@@ -25,15 +25,21 @@ 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)
|
||||
cy.contains('td', teacher1)
|
||||
})
|
||||
|
||||
it('Can check teacher page', () => {
|
||||
cy.url().should('include', '/teachers')
|
||||
cy.get('.borderless > :nth-child(3)').click()
|
||||
cy.get('.prompt').type(teacher1.split()[0])
|
||||
cy.contains('td', teacher1).click()
|
||||
cy.contains('Aineopintojen harjoitustyö: Tietokantasovellus')
|
||||
cy.visit('/teachers/hy-hlo-51367956')
|
||||
cy.contains('Landgraf Leo')
|
||||
cy.get('.ok-sortable-table tbody tr')
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.contains('td:nth-child(1)', 'TKT20011')
|
||||
cy.contains('td:nth-child(2)', 'Aineopintojen harjoitustyö: Tietokantasovellus')
|
||||
cy.contains('td:nth-child(3)', '96')
|
||||
cy.contains('td:nth-child(4)', '0')
|
||||
cy.contains('td:nth-child(5)', '88.89%')
|
||||
})
|
||||
})
|
||||
|
||||
it('Check leaderboad works', () => {
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
require('@cypress/code-coverage/support')
|
||||
require('./commands')
|
||||
|
||||
// This error is benign and seems to be only present in Cypress
|
||||
|
||||
@@ -44,7 +44,7 @@ services:
|
||||
<<: *defaults
|
||||
command: >
|
||||
sh -c 'redis-server --appendonly yes && redis-cli flushall'
|
||||
image: redis:6.2.13
|
||||
image: redis:6.2.14
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
@@ -81,7 +81,7 @@ services:
|
||||
|
||||
jami-db:
|
||||
<<: *defaults
|
||||
image: postgres:13.2
|
||||
image: postgres:15.7
|
||||
environment:
|
||||
- PGDATA=/data
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
@@ -161,6 +161,6 @@ services:
|
||||
<<: *defaults
|
||||
command: '-cid sis-updater-nats -m 8222 -p 4222 --max_channels 1000 --max_age 2h --hb_fail_count 2 --hb_timeout 10s --hb_interval 10s --auth dev --file_slice_max_bytes 0 --file_slice_max_age 60s --max_msgs 0 -store file -dir datastore'
|
||||
container_name: sis-updater-nats
|
||||
image: nats-streaming:0.16.2
|
||||
image: nats-streaming:0.25.6
|
||||
profiles:
|
||||
- updater
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
x-defaults: &defaults
|
||||
restart: unless-stopped
|
||||
|
||||
x-build_defaults: &build_defaults
|
||||
dockerfile: dev.Dockerfile
|
||||
|
||||
services:
|
||||
adminer:
|
||||
<<: *defaults
|
||||
container_name: adminer
|
||||
environment:
|
||||
ADMINER_DESIGN: pepa-linha
|
||||
image: adminer:4.8.1
|
||||
ports:
|
||||
- 5050:8080
|
||||
volumes:
|
||||
- ./adminer-plugins:/var/www/html/plugins-enabled
|
||||
|
||||
backend:
|
||||
<<: *defaults
|
||||
build:
|
||||
<<: *build_defaults
|
||||
context: services/backend
|
||||
depends_on:
|
||||
- kone-db
|
||||
- redis
|
||||
- sis-db
|
||||
- user-db
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
DB_URL_KONE: postgres://postgres@kone-db:5432/kone-db
|
||||
DB_URL_USER: postgres://postgres@user-db:5432/user-db
|
||||
FRONT_URL: http://localhost:3000
|
||||
REDIS: redis
|
||||
SECRET_TOKEN: dev
|
||||
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db
|
||||
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
|
||||
IMPORTER_DB_API_URL: 'https://localhost:3005'
|
||||
JAMI_URL: 'http://jami:3003/'
|
||||
SERVICE_PROVIDER: 'Fd'
|
||||
SIS_HOST: ${SIS_HOST}
|
||||
SIS_GRAPHQL_APPLICATION_ACCOUNT: ${SIS_GRAPHQL_APPLICATION_ACCOUNT}
|
||||
SIS_GRAPHQL_APPLICATION_KEY: ${SIS_GRAPHQL_APPLICATION_KEY}
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- ./environments/ha:/opt/app-root/src/src/environment
|
||||
- ./services/backend/src:/opt/app-root/src/src
|
||||
frontend:
|
||||
<<: *defaults
|
||||
build:
|
||||
<<: *build_defaults
|
||||
context: services/frontend
|
||||
container_name: frontend
|
||||
ports:
|
||||
- 3000:3000
|
||||
environment:
|
||||
REACT_APP_SERVICE_PROVIDER: 'Fd'
|
||||
volumes:
|
||||
- ./services/frontend/src/:/opt/app-root/src/src
|
||||
|
||||
kone-db:
|
||||
<<: *defaults
|
||||
container_name: kone-db
|
||||
environment:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: kone-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: postgres:15.7
|
||||
ports:
|
||||
- 12349:5432
|
||||
volumes:
|
||||
- kone-db-data:/data
|
||||
|
||||
redis:
|
||||
<<: *defaults
|
||||
command: 'redis-server --appendonly yes'
|
||||
container_name: redis
|
||||
image: redis:6.2.14
|
||||
ports:
|
||||
- '6379:6379'
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
sis-db:
|
||||
<<: *defaults
|
||||
container_name: sis-db
|
||||
environment:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: sis-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: postgres:15.7
|
||||
volumes:
|
||||
- sis-db-data:/data
|
||||
shm_size: 2gb
|
||||
ports:
|
||||
- 12345:5432
|
||||
|
||||
sis-updater-nats:
|
||||
<<: *defaults
|
||||
command: '-cid sis-updater-nats -m 8222 -p 4222 --max_channels 1000 --max_age 2h --hb_fail_count 2 --hb_timeout 10s --hb_interval 10s --auth dev --file_slice_max_bytes 0 --file_slice_max_age 60s --max_msgs 0 -store file -dir datastore'
|
||||
container_name: sis-updater-nats
|
||||
image: nats-streaming:0.25.6
|
||||
|
||||
sis-updater-scheduler:
|
||||
<<: *defaults
|
||||
build:
|
||||
<<: *build_defaults
|
||||
context: updater/sis-updater-scheduler
|
||||
container_name: sis-updater-scheduler
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
depends_on:
|
||||
- redis
|
||||
- sis-updater-nats
|
||||
environment:
|
||||
REDIS_URI: //redis:6379
|
||||
SECRET_TOKEN: dev
|
||||
SIS_IMPORTER_HOST: ${SIS_IMPORTER_DB_HOST:-host.docker.internal}
|
||||
SIS_IMPORTER_PORT: ${SIS_IMPORTER_DB_PORT:-15234}
|
||||
SIS_IMPORTER_DATABASE: importer-db
|
||||
SIS_IMPORTER_PASSWORD: dev
|
||||
SIS_IMPORTER_USER: dev
|
||||
SIS_NATS_TOKEN: dev
|
||||
SIS_NATS_URI: sis-updater-nats://sis-updater-nats:4222
|
||||
ports:
|
||||
- 8082:8082
|
||||
volumes:
|
||||
- ./updater/sis-updater-scheduler/:/opt/app-root/src
|
||||
|
||||
sis-updater-worker:
|
||||
<<: *defaults
|
||||
build:
|
||||
<<: *build_defaults
|
||||
context: updater/sis-updater-worker
|
||||
depends_on:
|
||||
- redis
|
||||
- sis-db
|
||||
- sis-updater-nats
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
environment:
|
||||
DB_URL: postgres://postgres@sis-db:5432/sis-db
|
||||
REDIS_URI: //redis:6379
|
||||
SIS_IMPORTER_HOST: ${SIS_IMPORTER_DB_HOST:-host.docker.internal}
|
||||
SIS_IMPORTER_PORT: ${SIS_IMPORTER_DB_PORT:-15234}
|
||||
SIS_IMPORTER_DATABASE: importer-db
|
||||
SIS_IMPORTER_PASSWORD: dev
|
||||
SIS_IMPORTER_USER: dev
|
||||
SIS_PASSWORD: postgres
|
||||
SIS_NATS_TOKEN: dev
|
||||
SIS_NATS_URI: sis-updater-nats://sis-updater-nats:4222
|
||||
ROOT_ORG_ID: ${ROOT_ORG_ID}
|
||||
volumes:
|
||||
- ./updater/sis-updater-worker/:/opt/app-root/src
|
||||
|
||||
user-db:
|
||||
<<: *defaults
|
||||
container_name: user-db
|
||||
environment:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: user-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: postgres:15.7
|
||||
volumes:
|
||||
- user-db-data:/data
|
||||
ports:
|
||||
- 12347:5432
|
||||
|
||||
volumes:
|
||||
kone-db-data:
|
||||
redis-data:
|
||||
sis-db-data:
|
||||
user-db-data:
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: oodikone
|
||||
importer:
|
||||
name: importer_network
|
||||
external: true
|
||||
@@ -0,0 +1,10 @@
|
||||
services:
|
||||
backend:
|
||||
volumes:
|
||||
- backend-nodemod:/opt/app-root/src/node_modules
|
||||
- ./services/backend/instrumented:/opt/app-root/src/src
|
||||
|
||||
frontend:
|
||||
volumes:
|
||||
- frontend-nodemod:/opt/app-root/src/node_modules
|
||||
- ./services/frontend/instrumented:/opt/app-root/src/src
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '2.1'
|
||||
|
||||
x-defaults: &defaults
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -12,7 +10,7 @@ services:
|
||||
container_name: adminer
|
||||
environment:
|
||||
ADMINER_DESIGN: pepa-linha
|
||||
image: adminer:4.8.0
|
||||
image: adminer:4.8.1
|
||||
ports:
|
||||
- 5050:8080
|
||||
volumes:
|
||||
@@ -30,6 +28,7 @@ services:
|
||||
- user-db
|
||||
env_file:
|
||||
- .env
|
||||
tty: true
|
||||
environment:
|
||||
DB_URL_KONE: postgres://postgres@kone-db:5432/kone-db
|
||||
DB_URL_USER: postgres://postgres@user-db:5432/user-db
|
||||
@@ -75,7 +74,7 @@ services:
|
||||
<<: *defaults
|
||||
command: 'redis-server --appendonly yes'
|
||||
container_name: redis
|
||||
image: redis:6.2.13
|
||||
image: redis:6.2.14
|
||||
ports:
|
||||
- '6379:6379'
|
||||
volumes:
|
||||
@@ -106,7 +105,7 @@ services:
|
||||
<<: *defaults
|
||||
command: '-cid sis-updater-nats -m 8222 -p 4222 --max_channels 1000 --max_age 2h --hb_fail_count 2 --hb_timeout 10s --hb_interval 10s --auth dev --file_slice_max_bytes 0 --file_slice_max_age 60s --max_msgs 0 -store file -dir datastore'
|
||||
container_name: sis-updater-nats
|
||||
image: nats-streaming:0.16.2
|
||||
image: nats-streaming:0.25.6
|
||||
profiles:
|
||||
- updater
|
||||
|
||||
@@ -188,7 +187,7 @@ services:
|
||||
|
||||
jami-db:
|
||||
<<: *defaults
|
||||
image: postgres:13.2
|
||||
image: postgres:15.7
|
||||
environment:
|
||||
- PGDATA=/data
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Documentation
|
||||
|
||||
Oodikone public documentation. For internal documentation, see Toska GitLab.
|
||||
@@ -25,7 +25,7 @@ Basic rule of thumb is that for the master/licentiate programme I used names `co
|
||||
|
||||
All things listed below should work automatically, if new combinations are done.
|
||||
|
||||
- Credits produced byt the studyprogramme
|
||||
- Credits produced by the studyprogramme
|
||||
- credits are computed for both programmes separately
|
||||
- Average graduation times
|
||||
- For bachelor + master/licentiate studyright the time is the whole study time starting from bachelor
|
||||
@@ -50,7 +50,7 @@ All things listed below should work automatically, if new combinations are done.
|
||||
|
||||
- Possibility to add criteria for whole bachelor-master study time
|
||||
- For eläinlääkis this criteria is saved by bachelor programme code. Other cases may need other approaches.
|
||||
In eläinklääkis this done by adding the criteria for 6 years -> the whole study time criteria is saved with the bachelor programme code. That is, same criteria is found, if bachelor or combined programme view is shown. This approach might need extra thinking for different programmes.
|
||||
In eläinlääkis this done by adding the criteria for 6 years -> the whole study time criteria is saved with the bachelor programme code. That is, same criteria is found, if bachelor or combined programme view is shown. This approach might need extra thinking for different programmes.
|
||||
- Show own tables for both programmes
|
||||
- To fetch data **needs manual work** here `services/backend/src/services/studyprogrammeHelpers.js` in the constant `combinedStudyprogrammes`.
|
||||
|
||||
@@ -93,4 +93,4 @@ All things listed below should work automatically, if new combinations are done.
|
||||
|
||||
- Backend: student data is fetched by bachelor study programme code in `/v3/populationstatisticsbystudentnumbers`. Separation from code 'KHxx_xxx+MHxx_xxx' is done in backend.
|
||||
- Frontend: things are done in the same way as with class stats.
|
||||
- **Need manual work**: `./oodikone/services/frontend/src/redux/elementdetails.js` add combined programme codes (both bachelor and master) to `combinedProgrammeCodes` and modify `combinedOptions` list accordinly in `useFilterAndFormattedElementDetails()` function.
|
||||
- **Need manual work**: `./oodikone/services/frontend/src/redux/elementdetails.js` add combined programme codes (both bachelor and master) to `combinedProgrammeCodes` and modify `combinedOptions` list accordingly in `useFilterAndFormattedElementDetails()` function.
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
# Databases
|
||||
|
||||
Oodikone uses three separate databases, each for different purposes. This document contains a simplified overview of each database.
|
||||
|
||||
Please note that each database may contain additional information such as migration tables or indices that are not depicted here. For more details, inspect the actual databases via the commandline.
|
||||
|
||||
In the tables, `PK` marks primary key(s), `FK` marks foreign keys and `UK` mark unique keys. Refer to the official [Mermaid.js documentation](https://mermaid.js.org/syntax/entityRelationshipDiagram.html) for full syntax.
|
||||
|
||||
## kone-db
|
||||
|
||||
The `kone-db` database contains information used in various features of Oodikone. This functionality is native to Oodikone and not necessarily directly related to Sisu.
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
custom_population_searches {
|
||||
bigint id PK "not null"
|
||||
bigint userId
|
||||
varchar(255) name "not null"
|
||||
varchar(255)[] students
|
||||
timestamp createdAt
|
||||
timestamp updatedAt
|
||||
}
|
||||
excluded_courses {
|
||||
integer id PK "not null"
|
||||
varchar(255) programme_code
|
||||
varchar(255) course_code
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
varchar(255) curriculum_version
|
||||
}
|
||||
open_uni_population_searches {
|
||||
bigint id PK "not null"
|
||||
bigint user_id
|
||||
varchar(255) name "not null"
|
||||
varchar(255)[] course_codes
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
progress_criteria {
|
||||
varchar(255) code PK "not null"
|
||||
varchar(255)[] courses_year_one
|
||||
varchar(255)[] courses_year_two
|
||||
varchar(255)[] courses_year_three
|
||||
integer credits_year_one
|
||||
integer credits_year_two
|
||||
integer credits_year_three
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
varchar(255)[] courses_year_four
|
||||
varchar(255)[] courses_year_five
|
||||
varchar(255)[] courses_year_six
|
||||
integer credits_year_four
|
||||
integer credits_year_five
|
||||
integer credits_year_six
|
||||
varchar(255) curriculum_version
|
||||
}
|
||||
study_guidance_group_tags {
|
||||
bigint id PK "no null"
|
||||
varchar(255) study_guidance_group_id UK
|
||||
varchar(255) study_programme
|
||||
varchar(255) year
|
||||
timestamp created_at
|
||||
timestamp updated_at
|
||||
}
|
||||
study_programme_pins {
|
||||
integer user_id PK "not null"
|
||||
varchar(255)[] study_programmes
|
||||
}
|
||||
tag ||--o{ tag_student : "belongs to"
|
||||
tag {
|
||||
timestamp createdAt
|
||||
timestamp updatedAt
|
||||
varchar(255) tagname
|
||||
varchar(255) studytrack "not null"
|
||||
bigint tag_id PK "not null"
|
||||
varchar(255) year
|
||||
bigint personal_user_id
|
||||
}
|
||||
tag_student {
|
||||
timestamp createdAt
|
||||
timestamp updatedAt
|
||||
varchar(255) studentnumber PK "not null"
|
||||
bigint tag_id PK, FK "not null"
|
||||
}
|
||||
```
|
||||
|
||||
## user-db
|
||||
|
||||
The `user-db` database contains information about users of Oodikone.
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
users {
|
||||
bigint id PK "not null"
|
||||
varchar(255) full_name
|
||||
varchar(255) username UK
|
||||
varchar(255) language "default: 'fi'"
|
||||
varchar(255) email
|
||||
varchar(255) sisu_person_id
|
||||
timestamp last_login
|
||||
varchar(255)[] roles "not null, default: []"
|
||||
varchar(255)[] programme_rights "not null, default: []"
|
||||
}
|
||||
```
|
||||
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
@@ -1,48 +1,105 @@
|
||||
# Mystery numbers
|
||||
# Numeric types
|
||||
|
||||
Sis data uses some numeric classifiers that are not self-explanatory and (most) previously needed to be deciphered from existing code.
|
||||
Some mystery numbers are gathered here for convenience. Feel free to add anything that could be useful. Remember to use the spelling used in code to enable project wide search.
|
||||
Data used in Oodikone contains some numeric types (or _magic numbers_) that are not self-explanatory. The values of the numeric types are gathered here for convenience and quick reference.
|
||||
|
||||
## extentcode
|
||||
## Extent code
|
||||
|
||||
(from **studyright**)
|
||||
Appears in model Studyright (db: `studyright`) as _extentcode_ (db: `extentcode`) and in model SISStudyRight (db: `sis_study_rights`) as _extentCode_ (db: `extent_code`). Values are defined in the database table `studyright_extents`.
|
||||
|
||||
These are actually defined in db table studyright_extents. Tells studyright level or type, eg. bachelor's degree or non-degree studies.
|
||||
| value | meaning |
|
||||
| ----: | :-------------------------------------------------- |
|
||||
| 1 | Bachelor's degree |
|
||||
| 2 | Master's degree |
|
||||
| 3 | Licentiate |
|
||||
| 4 | Doctor |
|
||||
| 5 | Bachelor's and Master's degree |
|
||||
| 6 | Continuing education |
|
||||
| 7 | Exchange studies |
|
||||
| 9 | Open University studies |
|
||||
| 13 | Non-degree pedagogical studies for teachers |
|
||||
| 14 | Contract training |
|
||||
| 16 | Studies for secondary school students |
|
||||
| 18 | Specialisation studies |
|
||||
| 22 | Non-degree programme for special education teachers |
|
||||
| 23 | Specialist training in medicine and dentistry |
|
||||
| 31 | Summer and winter school |
|
||||
| 34 | Exchange studies (postgraduate studies) |
|
||||
| 99 | Non-Degree studies |
|
||||
|
||||
## prioritycode
|
||||
## Priority code
|
||||
|
||||
(from **studyright**)
|
||||
> [!WARNING]
|
||||
> Obsolete, to be deprecated
|
||||
|
||||
1: Primary
|
||||
Appears in model Studyright (db: `studyright`) as _prioritycode_ (db: `prioritycode`).
|
||||
|
||||
2: Secondary
|
||||
| value | meaning | notes |
|
||||
| ----: | :-------- | :------------------------------------------------------------------------------------------ |
|
||||
| 1 | Primary | |
|
||||
| 2 | Secondary | |
|
||||
| 5 | Rescinded | seemingly not in use anymore |
|
||||
| 6 | Option | Ba+Ma students can add a master programme to their study plan before graduating as bachelor |
|
||||
| 30 | Graduated | |
|
||||
|
||||
5: Rescinded (Seemingly not in use anymore)
|
||||
## Type
|
||||
|
||||
6: Option (Bachelor+master studyright students can add a master programme to their study plan before graduation in bachelor programme: marked as option in Oodikone)
|
||||
> [!WARNING]
|
||||
> Obsolete, to be deprecated
|
||||
|
||||
30: Graduated
|
||||
Appears in model ElementDetails (db: `element_details`) as _type_ (db: `type`).
|
||||
|
||||
## type
|
||||
| value | meaning |
|
||||
| ----: | :----------------------------------------------- |
|
||||
| 20 | study programme |
|
||||
| 30 | module, studytrack or kokonaisuus e.g. neurology |
|
||||
|
||||
(from **element_details**)
|
||||
## Phase
|
||||
|
||||
20: A study programme
|
||||
Appears in model SISStudyRightElement (db: `sis_study_right_elements`) as _phase_ (db: `phase`).
|
||||
|
||||
30: Module/Studytrack/kokonaisuus? Eg. Neurology
|
||||
In Finland it is common for new students to be awarded the study right to both a bachelor's and a master's programme when they are admitted to the university. In Oodikone these two-phase study rights are marked with the [extent code](#extent-code) 5. In the study right elements of these study rights, phase 2 marks the second phase of the study right.
|
||||
|
||||
## enrollmenttype
|
||||
For example: Student is awarded the study right in the bachelor's programme in computer science with the extent code 5. The study right element for study right in the bachelor's programme is marked with phase 1. After graduating from the bachelor's programme, the student continues their studies in the master's programme in computer science. The new study right element for the study right in the master's programme is marked with phase 2.
|
||||
|
||||
(from **semester_enrollments**)
|
||||
All study right elements for other study rights (extent code other than 5) are marked with phase 1.
|
||||
|
||||
1: Present
|
||||
The level of the programme (bachelor's, master's etc.) of the study right can be directly checked from degreeProgrammeType (db: `degree_programme_type`) in model SISStudyRightElement (db: `sis_study_right_elements`).
|
||||
|
||||
2: Absent
|
||||
| value | meaning |
|
||||
| ----: | :------- |
|
||||
| 1 | any |
|
||||
| 2 | master's |
|
||||
|
||||
3: Inactive (neither 1 or 2 has been declared)
|
||||
## Enrollment type
|
||||
|
||||
## credittypecode
|
||||
Appears in model SemesterEnrollment (db: `semester_enrollments`) as _enrollmenttype_ (db: `enrollmenttype`).
|
||||
|
||||
(from **credit**)
|
||||
| value | meaning | notes |
|
||||
| ----: | :------- | :------------------------------- |
|
||||
| 1 | Present | |
|
||||
| 2 | Absent | |
|
||||
| 3 | Inactive | neither 1 or 2 has been declared |
|
||||
|
||||
Defined in credit_types db table. Completed/Improved/Transferred/Failed
|
||||
## Credit type code
|
||||
|
||||
Appears in model Credit (db: `credit`) as _credittypecode_ (db: `credittypecode`). Values are defined in the database table `credit_types`.
|
||||
|
||||
Completed/Improved/Transferred/Failed
|
||||
|
||||
| value | meaning |
|
||||
| ----: | :--------------- |
|
||||
| 4 | Completed |
|
||||
| 7 | Improved (grade) |
|
||||
| 9 | Transferred |
|
||||
| 10 | Failed |
|
||||
|
||||
## Gender code
|
||||
|
||||
Appears in model Student (db: `student`) as _gender_code_ (db: `gender_code`).
|
||||
|
||||
| value | meaning |
|
||||
| ----: | :------ |
|
||||
| 0 | Unknown |
|
||||
| 1 | Male |
|
||||
| 2 | Female |
|
||||
| 3 | Other |
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Resources
|
||||
|
||||
A collection of useful resources and documentation.
|
||||
|
||||
## AI tools
|
||||
|
||||
- [ChatGPT](https://chatgpt.com)
|
||||
- [CurreChat](https://curre.helsinki.fi/chat)
|
||||
- [GitHub Copilot](https://github.com/features/copilot)
|
||||
- Free for students!
|
||||
- [Microsoft Copilot](https://copilot.microsoft.com)
|
||||
|
||||
## Code style and maintainability
|
||||
|
||||
- [CodeClimate documentation](https://docs.codeclimate.com/docs)
|
||||
- [Advanced configuration](https://docs.codeclimate.com/docs/advanced-configuration)
|
||||
- [ESLint documentation](https://eslint.org/docs/v8.x)
|
||||
- [Prettier documentation](https://prettier.io/docs/en)
|
||||
|
||||
## Database
|
||||
|
||||
- [Postgres documentation](https://www.postgresql.org/docs/15/index.html)
|
||||
- [Sequelize documentation](https://sequelize.org/docs/v6/)
|
||||
|
||||
## GitHub
|
||||
|
||||
- [GitHub documentation](https://docs.github.com/en)
|
||||
- [GitHub Actions](https://docs.github.com/en/actions)
|
||||
- [GitHub Education](https://github.com/education/students)
|
||||
- Free benefits for students!
|
||||
- [Mermaid.js documentation](https://mermaid.js.org/intro/syntax-reference.html)
|
||||
|
||||
## TypeScript
|
||||
|
||||
- [TypeScript documentation](https://www.typescriptlang.org/docs)
|
||||
- [`tsconfig` reference](https://www.typescriptlang.org/tsconfig)
|
||||
- [TypeScript instructions for Sequelize](https://sequelize.org/docs/v6/other-topics/typescript)
|
||||
- [`sequelize-typescript` instructions](https://github.com/sequelize/sequelize-typescript?tab=readme-ov-file#readme)
|
||||
|
||||
## User interface
|
||||
|
||||
- [Semantic UI React documentation](https://react.semantic-ui.com)
|
||||
|
||||
## Visual Studio Code
|
||||
|
||||
_Todo: add relevant plugins_
|
||||
|
||||
## Web technologies
|
||||
|
||||
- [MDN Web Docs](https://developer.mozilla.org/en-US/)
|
||||
@@ -0,0 +1,3 @@
|
||||
# Tietosuoja
|
||||
|
||||
Oodikoneessa noudatetaan Helsingin yliopiston [yleistä linjausta](https://www.helsinki.fi/fi/tutustu-meihin/tietosuoja#opiskelijoiden-henkil-tietojen-k-sittely--title) opiskelijoiden henkilötietojen käsittelystä.
|
||||
@@ -0,0 +1,7 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
const { parserPlugins } = require('@istanbuljs/schema').defaults.nyc
|
||||
|
||||
module.exports = {
|
||||
cache: false,
|
||||
parserPlugins: parserPlugins.concat('typescript', 'jsx'),
|
||||
}
|
||||
@@ -3,14 +3,16 @@
|
||||
"description": "An application for analyzing university data.",
|
||||
"scripts": {
|
||||
"cypress": "cypress",
|
||||
"instrument": "./scripts/instrument.sh",
|
||||
"eslint": "eslint",
|
||||
"lint:all": "eslint --ext .js,.jsx services/ updater/ --report-unused-disable-directives",
|
||||
"lint:all": "eslint --ext .js,.jsx,.ts,.tsx services/ updater/ --report-unused-disable-directives",
|
||||
"stylelint": "stylelint",
|
||||
"actionlint": "node-actionlint",
|
||||
"prettier": "prettier",
|
||||
"cli": "./cli.sh",
|
||||
"oodikone": "./run.sh oodikone anon up --detach",
|
||||
"oodikone:real": "./run.sh oodikone real up --detach",
|
||||
"oodikone:test": "./run.sh oodikone test up --detach",
|
||||
"updater": "./run.sh updater anon up --detach",
|
||||
"updater:real": "./run.sh updater real up --detach",
|
||||
"both": "./run.sh both anon up --detach",
|
||||
@@ -18,7 +20,7 @@
|
||||
"docker:down": "./run.sh both anon down --remove-orphans; ./run.sh both real down --remove-orphans",
|
||||
"testupdater": "docker-compose --file docker-compose.ci.yml up --detach --no-recreate sis-updater-worker && SCHEDULE_IMMEDIATE=weekly EXIT_AFTER_IMMEDIATES=yes docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start",
|
||||
"flushredis": "docker exec -it redis redis-cli flushall",
|
||||
"prepare": "husky install",
|
||||
"prepare": "husky",
|
||||
"rapodiff": "docker exec oodikone_backend_1 node /opt/app-root/src/src/rapodiff/rapodiff.js",
|
||||
"install:local": "npm i && cd services/frontend && npm i && cd ../backend && npm i && cd ../../updater/sis-updater-scheduler && npm i && cd ../sis-updater-worker && npm i && cd ../..",
|
||||
"install:docker": "docker exec frontend npm i && docker exec oodikone_backend_1 npm i && docker exec sis-updater-scheduler npm i && docker exec oodikone_sis-updater-worker_1 npm i",
|
||||
@@ -34,21 +36,27 @@
|
||||
},
|
||||
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
|
||||
"devDependencies": {
|
||||
"@cypress/code-coverage": "^3.12.39",
|
||||
"@istanbuljs/schema": "^0.1.3",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||
"@typescript-eslint/parser": "^7.16.1",
|
||||
"cypress": "^13.2.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-custom-alias": "^1.3.2",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"eslint-plugin-cypress": "^3.0.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"husky": "^8.0.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"husky": "^9.0.0",
|
||||
"lint-staged": "^15.0.0",
|
||||
"node-actionlint": "^1.2.2",
|
||||
"prettier": "3.2.5",
|
||||
"nyc": "^17.0.0",
|
||||
"prettier": "3.3.0",
|
||||
"stylelint": "^16.2.1",
|
||||
"stylelint-config-standard": "^36.0.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "all non-major dependencies",
|
||||
"groupSlug": "all-minor-patch",
|
||||
"matchPackagePatterns": ["*"],
|
||||
"matchUpdateTypes": ["minor", "patch"]
|
||||
},
|
||||
{
|
||||
"matchDatasources": ["docker"],
|
||||
"matchPackageNames": ["node"],
|
||||
"versionCompatibility": "^(?<version>[^-]+)(?<compatibility>-.*)?$",
|
||||
"versioning": "node"
|
||||
}
|
||||
],
|
||||
"rebaseWhen": "never"
|
||||
}
|
||||
@@ -47,7 +47,7 @@ parse_params() {
|
||||
compose_command=${args[*]:1}
|
||||
else
|
||||
version=${args[1]}
|
||||
[[ "$version" != "anon" && "$version" != "real" ]] && usage && \
|
||||
[[ "$version" != "anon" && "$version" != "real" && "$version" != "test" ]] && usage && \
|
||||
die "Wrong version $version"
|
||||
compose_command=${args[*]:2}
|
||||
fi
|
||||
@@ -70,6 +70,9 @@ parse_env() {
|
||||
if [[ "$version" == "real" ]]; then
|
||||
env="--file docker-compose.yml --file docker-compose.real.yml"
|
||||
fi
|
||||
if [[ "$version" == "test" ]]; then
|
||||
env="--file docker-compose.yml --file docker-compose.test.yml"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script is used for instrumenting the frontend code
|
||||
# Counters are added for calculating the code coverage
|
||||
# Instrumented code is used for generating code coverage reports
|
||||
|
||||
# Ensure the script exits if any command fails
|
||||
set -euo pipefail
|
||||
|
||||
# Print status
|
||||
echo "Instrumenting code..."
|
||||
|
||||
# Define the directories
|
||||
SOURCE_DIR="services/frontend/src"
|
||||
INSTRU_DIR="services/frontend/instrumented"
|
||||
|
||||
# Create the instrumented directory if it does not exist
|
||||
mkdir -p $INSTRU_DIR
|
||||
|
||||
# Instrument the code
|
||||
nyc instrument --all --compact=false $SOURCE_DIR $INSTRU_DIR
|
||||
|
||||
# Copy remaining files without overwriting instrumented files
|
||||
#
|
||||
# Images, CSS and other static files are not instrumended
|
||||
# but they are still needed to run the application
|
||||
rsync -av --quiet --ignore-existing $SOURCE_DIR/ $INSTRU_DIR/
|
||||
|
||||
# Print status
|
||||
echo "Done!"
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM registry.access.redhat.com/ubi9/nodejs-18-minimal
|
||||
FROM registry.access.redhat.com/ubi9/nodejs-20-minimal
|
||||
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
@@ -21,8 +21,12 @@ ENV SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
|
||||
ARG SENTRY_RELEASE
|
||||
ENV SENTRY_RELEASE=$SENTRY_RELEASE
|
||||
|
||||
COPY ./package* ./
|
||||
RUN npm ci --omit-dev
|
||||
COPY ./package*.json ./
|
||||
|
||||
RUN npm ci --include=dev
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["node", "index.js"]
|
||||
RUN npm run build
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,141 +0,0 @@
|
||||
// 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',
|
||||
MH30_006: 'terv',
|
||||
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',
|
||||
MH80_008: 'emplant',
|
||||
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-dp',
|
||||
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 }
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:18-alpine
|
||||
FROM node:20-alpine
|
||||
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
@@ -13,4 +13,4 @@ COPY ./package* ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
|
||||
CMD ["node_modules/.bin/nodemon", "index.js"]
|
||||
CMD ["node_modules/.bin/tsx", "watch", "--clear-screen=false", "index.js"]
|
||||
|
||||
@@ -4,3 +4,6 @@ const logger = require('./src/util/logger')
|
||||
process.on('unhandledRejection', reason => {
|
||||
logger.error({ message: 'unhandledRejection: ', meta: reason })
|
||||
})
|
||||
|
||||
process.on('SIGTERM', () => process.exit())
|
||||
process.on('SIGINT', () => process.exit())
|
||||
|
||||
@@ -2,36 +2,44 @@
|
||||
"name": "backend",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node --max_old_space_size=4096 index.js",
|
||||
"dev": "nodemon --max_old_space_size=4096 index.js",
|
||||
"build": "tsc",
|
||||
"rapodiff": "node /opt/app-root/src/src/rapodiff.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/node": "^5.30.0",
|
||||
"@sentry/tracing": "^6.18.2",
|
||||
"axios": "^0.21.4",
|
||||
"axios": "^0.28.1",
|
||||
"bluebird": "^3.7.2",
|
||||
"bullmq": "^4.14.2",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^1.8.2",
|
||||
"express": "^4.19.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^5.1.1",
|
||||
"lru-cache": "^10.2.2",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.37",
|
||||
"morgan": "^1.10.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"pg": "^8.11.5",
|
||||
"redis": "^3.1.2",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"sequelize": "^6.37.3",
|
||||
"sequelize-typescript": "^2.1.6",
|
||||
"umzug": "^2.3.0",
|
||||
"unfuck-utf8-headers-middleware": "^1.0.1",
|
||||
"winston": "^3.8.2",
|
||||
"winston-gelf-transporter": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.0"
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "^20.14.2",
|
||||
"@types/umzug": "^2.3.7",
|
||||
"@types/validator": "^13.12.0",
|
||||
"tsx": "^4.16.2",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"overrides": {
|
||||
"winston-gelf-transporter": {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
require('express-async-errors')
|
||||
const express = require('express')
|
||||
const { baseUrl, backendPort } = require('./conf-backend')
|
||||
const routes = require('./routes')
|
||||
const { startCron } = require('./events')
|
||||
|
||||
const { baseUrl, backendPort } = require('./config')
|
||||
const { initializeDatabaseConnection, dbConnections } = require('./database/connection')
|
||||
const { startCron } = require('./events')
|
||||
const routes = require('./routes')
|
||||
const logger = require('./util/logger')
|
||||
require('./worker/worker')
|
||||
|
||||
@@ -30,7 +31,7 @@ initializeDatabaseConnection()
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
.catch(error => {
|
||||
process.exitCode = 1
|
||||
logger.error(e)
|
||||
logger.error(error)
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ const isProduction = !isStaging && process.env.NODE_ENV === 'production'
|
||||
// Sentry
|
||||
const sentryRelease = process.env.SENTRY_RELEASE || ''
|
||||
const sentryEnvironment = process.env.SENTRY_ENVIRONMENT || ''
|
||||
const sentryDSN = process.env.SENTRY_DSN || ''
|
||||
const runningInCI = process.env.CI === 'true'
|
||||
|
||||
// IAM group
|
||||
@@ -23,13 +24,24 @@ const jamiUrl = process.env.JAMI_URL || ''
|
||||
const importerUrl = process.env.IMPORTER_DB_API_URL || ''
|
||||
const importerToken = process.env.IMPORTER_DB_API_TOKEN || ''
|
||||
|
||||
// Sisu
|
||||
const sisUrl = process.env.SIS_HOST
|
||||
const sisGrapqlAppAccount = process.env.SIS_GRAPHQL_APPLICATION_ACCOUNT
|
||||
const sisGrapqlAppKey = process.env.SIS_GRAPHQL_APPLICATION_KEY
|
||||
|
||||
// Networking: Urls & ports
|
||||
const baseUrl = isDev ? '/api' : ''
|
||||
const frontUrl = process.env.FRONT_URL
|
||||
const backendPort = 8080
|
||||
|
||||
// System run in whose environment
|
||||
const serviceProvider = process.env.SERVICE_PROVIDER || 'Toska'
|
||||
|
||||
// Optional logout-url configuration
|
||||
const configLogoutUrl = serviceProvider === 'Toska' ? undefined : process.env.LOGOUT_URL
|
||||
|
||||
// Other stuff
|
||||
const { DB_URL_KONE, DB_URL_USER, SECRET_TOKEN, SIS_UPDATER_URL, CRYPT_KEY } = process.env
|
||||
const { DB_URL_KONE, DB_URL_USER, SECRET_TOKEN, SIS_DB_URL, SIS_UPDATER_URL, CRYPT_KEY } = process.env
|
||||
|
||||
let DB_MAX_CONNECTIONS = parseInt(process.env.DB_MAX_CONNECTIONS, 10)
|
||||
if (Number.isNaN(DB_MAX_CONNECTIONS)) {
|
||||
@@ -60,10 +72,12 @@ module.exports = {
|
||||
USER_PASSWORD,
|
||||
SIS_PASSWORD,
|
||||
requiredGroup,
|
||||
SIS_DB_URL,
|
||||
SIS_UPDATER_URL,
|
||||
runningInCI,
|
||||
sentryRelease,
|
||||
sentryEnvironment,
|
||||
sentryDSN,
|
||||
isProduction,
|
||||
pateToken,
|
||||
jamiUrl,
|
||||
@@ -73,4 +87,10 @@ module.exports = {
|
||||
importerUrl,
|
||||
importerToken,
|
||||
isDev,
|
||||
isStaging,
|
||||
serviceProvider,
|
||||
configLogoutUrl,
|
||||
sisUrl,
|
||||
sisGrapqlAppAccount,
|
||||
sisGrapqlAppKey,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Linter was not having any of this so neeeded to add resolveJsonModule:true to tsconfig
|
||||
// export const { facultyCodes, ignoredFacultyCodes, magicFacultyCode } = require('../environment/organizationConfig.json')
|
||||
|
||||
export { facultyCodes, ignoredFacultyCodes, magicFacultyCode } from '../environment/organizationConfig.json'
|
||||
@@ -0,0 +1,138 @@
|
||||
// Map programme codes to "letter indentifier"
|
||||
|
||||
export const programmeCodes = {
|
||||
KH10_001: 'TUK',
|
||||
MH10_001: 'TUM',
|
||||
KH20_001: 'ON',
|
||||
MH20_001: 'OTM',
|
||||
MH20_002: 'IBL',
|
||||
MH20_003: 'GGL',
|
||||
KH30_001: 'PSYKK',
|
||||
KH30_002: 'LOGOK',
|
||||
KH30_003: 'SPK',
|
||||
MH30_001: 'LL',
|
||||
MH30_002: 'TMED',
|
||||
MH30_003: 'HLL',
|
||||
MH30_004: 'PSYK',
|
||||
MH30_005: 'LOGO',
|
||||
MH30_006: 'TERV',
|
||||
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: 'MAREEES',
|
||||
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',
|
||||
MH50_014: 'EnCHiL',
|
||||
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_010: 'IMESS',
|
||||
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',
|
||||
MH80_008: 'emPlant',
|
||||
KH90_001: 'ELK',
|
||||
MH90_001: 'ELL',
|
||||
T920101: 'DPTHEOL',
|
||||
T920102: 'DPLAW',
|
||||
T920103: 'DPHISTCULT',
|
||||
T920104: 'DPLANG',
|
||||
T920105: 'SKY',
|
||||
T920106: 'DPSOCS',
|
||||
T920107: 'PYAM',
|
||||
T920108: 'DPE',
|
||||
T920109: 'SEDUCE',
|
||||
T920110: 'CLIC',
|
||||
T920111: 'PHILARTSOC',
|
||||
T921101: 'DPBM',
|
||||
T921102: 'KLTO',
|
||||
T921103: 'DocPop',
|
||||
T921104: 'FINDOS',
|
||||
T921105: 'DPDR',
|
||||
T921106: 'ILS',
|
||||
T921107: 'BM',
|
||||
T921108: 'CVM',
|
||||
T921109: 'DPHUB',
|
||||
T922101: 'LUOVA',
|
||||
T922102: 'DPPS',
|
||||
T922103: 'DENVI',
|
||||
T922104: 'AGFOREE',
|
||||
T922105: 'MBDP',
|
||||
T922106: 'FoodHealth',
|
||||
T923101: 'PAPU',
|
||||
T923102: 'GeoDoc',
|
||||
T923103: 'ATM-DP',
|
||||
T923104: 'CHEMS',
|
||||
T923105: 'DOMAST',
|
||||
T923106: 'MATRENA',
|
||||
T923107: 'DOCS',
|
||||
} as const
|
||||
@@ -0,0 +1,10 @@
|
||||
export const roles = [
|
||||
'admin',
|
||||
'courseStatistics',
|
||||
'facultyStatistics',
|
||||
'fullSisuAccess',
|
||||
'katselmusViewer',
|
||||
'openUniSearch',
|
||||
'studyGuidanceGroups',
|
||||
'teachers',
|
||||
] as const
|
||||
@@ -1,18 +1,59 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const EventEmitter = require('events')
|
||||
const Umzug = require('umzug')
|
||||
const conf = require('../conf-backend')
|
||||
const logger = require('../util/logger')
|
||||
import EventEmitter from 'events'
|
||||
import { Sequelize } from 'sequelize-typescript'
|
||||
import Umzug from 'umzug'
|
||||
|
||||
const { SIS_DB_URL } = process.env
|
||||
import conf from '../config'
|
||||
import {
|
||||
Course,
|
||||
CourseProvider,
|
||||
CourseType,
|
||||
Credit,
|
||||
CreditTeacher,
|
||||
CreditType,
|
||||
ElementDetail,
|
||||
Enrollment,
|
||||
Organization,
|
||||
ProgrammeModule,
|
||||
ProgrammeModuleChild,
|
||||
Semester,
|
||||
SemesterEnrollment,
|
||||
SISStudyRight,
|
||||
SISStudyRightElement,
|
||||
Student,
|
||||
Studyplan,
|
||||
Studyright,
|
||||
StudyrightElement,
|
||||
StudyrightExtent,
|
||||
Teacher,
|
||||
Transfer,
|
||||
} from '../models'
|
||||
import {
|
||||
CustomPopulationSearch,
|
||||
ExcludedCourse,
|
||||
OpenUniPopulationSearch,
|
||||
ProgressCriteria,
|
||||
StudyGuidanceGroupTag,
|
||||
StudyProgrammePin,
|
||||
Tag,
|
||||
TagStudent,
|
||||
} from '../models/kone'
|
||||
import { User } from '../models/user'
|
||||
|
||||
import logger from '../util/logger'
|
||||
|
||||
class DbConnection extends EventEmitter {
|
||||
sequelize: Sequelize
|
||||
|
||||
RETRY_ATTEMPTS: number
|
||||
|
||||
established: boolean
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.RETRY_ATTEMPTS = 15
|
||||
this.established = false
|
||||
|
||||
this.sequelize = new Sequelize(SIS_DB_URL, {
|
||||
this.sequelize = new Sequelize(conf.SIS_DB_URL!, {
|
||||
dialect: 'postgres',
|
||||
pool: {
|
||||
max: 25,
|
||||
@@ -22,6 +63,30 @@ class DbConnection extends EventEmitter {
|
||||
},
|
||||
logging: false,
|
||||
password: conf.SIS_PASSWORD,
|
||||
models: [
|
||||
Course,
|
||||
CourseProvider,
|
||||
CourseType,
|
||||
Credit,
|
||||
CreditTeacher,
|
||||
CreditType,
|
||||
ElementDetail,
|
||||
Enrollment,
|
||||
Organization,
|
||||
ProgrammeModule,
|
||||
ProgrammeModuleChild,
|
||||
Semester,
|
||||
SemesterEnrollment,
|
||||
SISStudyRight,
|
||||
SISStudyRightElement,
|
||||
Student,
|
||||
Studyplan,
|
||||
Studyright,
|
||||
StudyrightElement,
|
||||
StudyrightExtent,
|
||||
Teacher,
|
||||
Transfer,
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,40 +95,53 @@ class DbConnection extends EventEmitter {
|
||||
await this.sequelize.authenticate()
|
||||
this.emit('connect')
|
||||
this.established = true
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
if (attempt > this.RETRY_ATTEMPTS) {
|
||||
this.emit('error', e)
|
||||
this.emit('error', error)
|
||||
return
|
||||
}
|
||||
logger.error({ message: `Sis database connection failed! Attempt ${attempt}/${this.RETRY_ATTEMPTS}`, meta: e })
|
||||
logger.error({
|
||||
message: `Sis database connection failed! Attempt ${attempt}/${this.RETRY_ATTEMPTS}`,
|
||||
meta: error,
|
||||
})
|
||||
setTimeout(() => this.connect(attempt + 1), 1000 * attempt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Old-style kone + user db connections
|
||||
const sequelizeKone = new Sequelize(conf.DB_URL_KONE, {
|
||||
const sequelizeKone = new Sequelize(conf.DB_URL_KONE!, {
|
||||
schema: conf.DB_SCHEMA_KONE,
|
||||
searchPath: conf.DB_SCHEMA_KONE,
|
||||
logging: false,
|
||||
password: conf.KONE_PASSWORD,
|
||||
models: [
|
||||
CustomPopulationSearch,
|
||||
ExcludedCourse,
|
||||
OpenUniPopulationSearch,
|
||||
ProgressCriteria,
|
||||
StudyGuidanceGroupTag,
|
||||
StudyProgrammePin,
|
||||
Tag,
|
||||
TagStudent,
|
||||
],
|
||||
})
|
||||
|
||||
sequelizeKone.query(`SET SESSION search_path to ${conf.DB_SCHEMA_KONE}`)
|
||||
|
||||
const sequelizeUser = new Sequelize(conf.DB_URL_USER, {
|
||||
const sequelizeUser = new Sequelize(conf.DB_URL_USER!, {
|
||||
logging: false,
|
||||
password: conf.USER_PASSWORD,
|
||||
models: [User],
|
||||
})
|
||||
|
||||
const initializeDatabaseConnection = async () => {
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
const rounds = 60
|
||||
|
||||
for (const [seq, dbName] of [
|
||||
[sequelizeKone, 'kone-db'],
|
||||
[sequelizeUser, 'user-db'],
|
||||
]) {
|
||||
] as const) {
|
||||
logger.info(`Connecting to ${dbName}...`)
|
||||
for (let round = 1; round <= rounds; round++) {
|
||||
try {
|
||||
@@ -92,12 +170,11 @@ const initializeDatabaseConnection = async () => {
|
||||
params: [seq.getQueryInterface(), Sequelize],
|
||||
path: `${process.cwd()}/src/database/${migrationsFolder}`,
|
||||
pattern: /\.js$/,
|
||||
schema,
|
||||
},
|
||||
})
|
||||
try {
|
||||
const migrations = await migrator.up()
|
||||
logger.info({ message: `${dbName} migrations up to date: `, meta: migrations.map(m => m.file) })
|
||||
logger.info({ message: `${dbName} migrations up to date: `, meta: migrations.map(migration => migration.file) })
|
||||
} catch (error) {
|
||||
logger.error({ message: `${dbName} migrations failed`, meta: error })
|
||||
throw error
|
||||
@@ -107,9 +184,4 @@ const initializeDatabaseConnection = async () => {
|
||||
|
||||
const dbConnections = new DbConnection()
|
||||
|
||||
module.exports = {
|
||||
dbConnections,
|
||||
sequelizeKone,
|
||||
sequelizeUser,
|
||||
initializeDatabaseConnection,
|
||||
}
|
||||
export { dbConnections, sequelizeKone, sequelizeUser, initializeDatabaseConnection }
|
||||
@@ -1,13 +1,12 @@
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
return queryInterface.sequelize.transaction(async transaction => {
|
||||
await queryInterface.sequelize.query('ALTER TABLE tag_student DROP CONSTRAINT "tag_student_tag_id_fkey"', {
|
||||
transaction,
|
||||
})
|
||||
await queryInterface.removeConstraint('tag_student', 'tag_student_tag_id_fkey', { transaction })
|
||||
await queryInterface.sequelize.query('DELETE FROM tag_student WHERE tag_id IS NULL', { transaction })
|
||||
await queryInterface.removeConstraint('tag', 'tag_tag_id_key', { transaction })
|
||||
await queryInterface.addConstraint('tag', ['tag_id'], {
|
||||
await queryInterface.addConstraint('tag', {
|
||||
type: 'primary key',
|
||||
fields: ['tag_id'],
|
||||
transaction,
|
||||
})
|
||||
await queryInterface.sequelize.query(
|
||||
@@ -15,8 +14,9 @@ module.exports = {
|
||||
{ transaction }
|
||||
)
|
||||
await queryInterface.removeColumn('tag_student', 'id', { transaction })
|
||||
await queryInterface.addConstraint('tag_student', ['studentnumber', 'tag_id'], {
|
||||
await queryInterface.addConstraint('tag_student', {
|
||||
type: 'primary key',
|
||||
fields: ['studentnumber', 'tag_id'],
|
||||
transaction,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -29,7 +29,6 @@ CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
|
||||
-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner:
|
||||
--
|
||||
|
||||
COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';
|
||||
|
||||
|
||||
SET search_path = public, pg_catalog;
|
||||
@@ -52,7 +51,6 @@ CREATE TABLE access_groups (
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE access_groups OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: access_groups_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
|
||||
@@ -66,7 +64,6 @@ CREATE SEQUENCE access_groups_id_seq
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE access_groups_id_seq OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: access_groups_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
|
||||
@@ -87,7 +84,6 @@ CREATE TABLE affiliations (
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE affiliations OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: affiliations_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
|
||||
@@ -101,7 +97,6 @@ CREATE SEQUENCE affiliations_id_seq
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE affiliations_id_seq OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: affiliations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
|
||||
@@ -122,7 +117,6 @@ CREATE TABLE faculty_programmes (
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE faculty_programmes OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: hibernate_sequence; Type: SEQUENCE; Schema: public; Owner: postgres
|
||||
@@ -136,7 +130,6 @@ CREATE SEQUENCE hibernate_sequence
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE hibernate_sequence OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: hy_groups; Type: TABLE; Schema: public; Owner: postgres
|
||||
@@ -150,7 +143,6 @@ CREATE TABLE hy_groups (
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE hy_groups OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: hy_groups_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
|
||||
@@ -164,7 +156,6 @@ CREATE SEQUENCE hy_groups_id_seq
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE hy_groups_id_seq OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: hy_groups_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
|
||||
@@ -183,7 +174,6 @@ CREATE TABLE sequence (
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE sequence OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: user_accessgroup; Type: TABLE; Schema: public; Owner: postgres
|
||||
@@ -198,7 +188,6 @@ CREATE TABLE user_accessgroup (
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE user_accessgroup OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: user_accessgroup_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
|
||||
@@ -212,7 +201,6 @@ CREATE SEQUENCE user_accessgroup_id_seq
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE user_accessgroup_id_seq OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: user_accessgroup_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
|
||||
@@ -234,7 +222,6 @@ CREATE TABLE user_affiliation (
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE user_affiliation OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: user_affiliation_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
|
||||
@@ -248,7 +235,6 @@ CREATE SEQUENCE user_affiliation_id_seq
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE user_affiliation_id_seq OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: user_affiliation_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
|
||||
@@ -269,7 +255,6 @@ CREATE TABLE user_elementdetails (
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE user_elementdetails OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: user_faculties; Type: TABLE; Schema: public; Owner: postgres
|
||||
@@ -283,7 +268,6 @@ CREATE TABLE user_faculties (
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE user_faculties OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: user_hy_group; Type: TABLE; Schema: public; Owner: postgres
|
||||
@@ -298,7 +282,6 @@ CREATE TABLE user_hy_group (
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE user_hy_group OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: user_hy_group_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
|
||||
@@ -312,7 +295,6 @@ CREATE SEQUENCE user_hy_group_id_seq
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE user_hy_group_id_seq OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: user_hy_group_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
|
||||
@@ -332,7 +314,6 @@ CREATE TABLE user_unit (
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE user_unit OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: user_unit_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
|
||||
@@ -346,7 +327,6 @@ CREATE SEQUENCE user_unit_id_seq
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE user_unit_id_seq OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: user_unit_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
|
||||
@@ -368,7 +348,6 @@ CREATE TABLE users (
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE users OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
|
||||
@@ -382,7 +361,6 @@ CREATE SEQUENCE users_id_seq
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE users_id_seq OWNER TO postgres;
|
||||
|
||||
--
|
||||
-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
const { ARRAY, STRING } = require('sequelize')
|
||||
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.addColumn('users', 'roles', {
|
||||
type: ARRAY(STRING),
|
||||
allowNull: false,
|
||||
defaultValue: [],
|
||||
})
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE users
|
||||
SET roles = COALESCE(subquery.roles, '{}')
|
||||
FROM (
|
||||
SELECT uag."userId" AS "userId", ARRAY_AGG(ag.group_code) AS roles
|
||||
FROM user_accessgroup uag
|
||||
JOIN access_groups ag ON uag."accessGroupId" = ag.id
|
||||
GROUP BY uag."userId"
|
||||
) AS subquery
|
||||
WHERE users.id = subquery."userId"
|
||||
`)
|
||||
await queryInterface.addColumn('users', 'programme_rights', {
|
||||
type: ARRAY(STRING),
|
||||
allowNull: false,
|
||||
defaultValue: [],
|
||||
})
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE users
|
||||
SET programme_rights = subquery.programme_rights
|
||||
FROM (
|
||||
SELECT "userId", ARRAY_AGG("elementDetailCode") AS programme_rights
|
||||
FROM "user_elementdetails"
|
||||
GROUP BY "userId"
|
||||
) AS subquery
|
||||
WHERE users.id = subquery."userId"
|
||||
`)
|
||||
},
|
||||
down: async queryInterface => {
|
||||
await queryInterface.removeColumn('users', 'roles')
|
||||
await queryInterface.removeColumn('users', 'programme_rights')
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.dropTable('user_faculties')
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.dropTable('user_elementdetails')
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.dropTable('user_accessgroup')
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.dropTable('access_groups')
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.removeColumn('users', 'iam_groups')
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.dropTable('"Sessions"')
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"facultyCodes": [
|
||||
"H10",
|
||||
"H20",
|
||||
"H30",
|
||||
"H40",
|
||||
"H50",
|
||||
"H55",
|
||||
"H57",
|
||||
"H60",
|
||||
"H70",
|
||||
"H74",
|
||||
"H80",
|
||||
"H90",
|
||||
"H92",
|
||||
"H930",
|
||||
"H99",
|
||||
"Y",
|
||||
"Y01"
|
||||
],
|
||||
"magicFacultyCode": "H50",
|
||||
"ignoredFacultyCodes": ["Y", "H99", "Y01", "H92", "H930"]
|
||||
}
|
||||
@@ -1,23 +1,22 @@
|
||||
const { CronJob } = require('cron')
|
||||
const { refreshAssociationsInRedis } = require('./services/studyrights')
|
||||
const { getAllProgrammes } = require('./services/studyrights')
|
||||
const { updateBasicView, updateStudytrackView } = require('./services/studyprogramme/studyprogrammeUpdates')
|
||||
const { findAndSaveTeachers } = require('./services/topteachers')
|
||||
const { faculties } = require('./services/organisations')
|
||||
const { combinedStudyprogrammes } = require('./services/studyprogramme/studyprogrammeHelpers')
|
||||
|
||||
const { isProduction, runningInCI } = require('./config')
|
||||
const { getFaculties } = require('./services/faculty/facultyHelpers')
|
||||
const { updateFacultyOverview, updateFacultyProgressOverview } = require('./services/faculty/facultyUpdates')
|
||||
const { isProduction, runningInCI } = require('./conf-backend')
|
||||
const { getCurrentSemester } = require('./services/semesters')
|
||||
const logger = require('./util/logger')
|
||||
const { getAssociations } = require('./services/studyrights')
|
||||
const { redisClient } = require('./services/redis')
|
||||
const { computeLanguageCenterData, LANGUAGE_CENTER_REDIS_KEY } = require('./services/languageCenterData')
|
||||
const { jobMaker } = require('./worker/queue')
|
||||
const { deleteOutdatedUsers } = require('./services/userService')
|
||||
const {
|
||||
findStudentsCloseToGraduation,
|
||||
CLOSE_TO_GRADUATION_REDIS_KEY,
|
||||
} = require('./services/populations/closeToGraduation')
|
||||
const { redisClient } = require('./services/redis')
|
||||
const { getCurrentSemester } = require('./services/semesters')
|
||||
const { combinedStudyprogrammes, isRelevantProgramme } = require('./services/studyProgramme/studyProgrammeHelpers')
|
||||
const { updateBasicView, updateStudytrackView } = require('./services/studyProgramme/studyProgrammeUpdates')
|
||||
const { getAssociations, getProgrammesFromStudyRights, refreshAssociationsInRedis } = require('./services/studyrights')
|
||||
const { findAndSaveTeachers } = require('./services/topteachers')
|
||||
const { deleteOutdatedUsers } = require('./services/userService')
|
||||
const logger = require('./util/logger')
|
||||
const { jobMaker } = require('./worker/queue')
|
||||
|
||||
const schedule = (cronTime, func) => new CronJob({ cronTime, onTick: func, start: true, timeZone: 'Europe/Helsinki' })
|
||||
|
||||
@@ -28,37 +27,22 @@ const refreshStudyrightAssociations = async () => {
|
||||
|
||||
const refreshFaculties = async () => {
|
||||
logger.info('Adding jobs to refresh all faculties')
|
||||
const facultyList = (await faculties()).filter(f => !['Y', 'H99', 'Y01', 'H92', 'H930'].includes(f.code))
|
||||
for (const faculty of facultyList) {
|
||||
const faculties = await getFaculties()
|
||||
for (const faculty of faculties) {
|
||||
jobMaker.faculty(faculty.code)
|
||||
}
|
||||
}
|
||||
|
||||
const refreshFaculty = async code => {
|
||||
logger.info('Started updating faculty')
|
||||
const start = new Date().getTime()
|
||||
try {
|
||||
await updateFacultyOverview(code, 'ALL')
|
||||
await updateFacultyProgressOverview(code)
|
||||
const time = new Date().getTime() - start
|
||||
logger.info(`Updated faculty ${code} (took ${(time / 1000 / 60).toFixed(2)} minutes)`)
|
||||
} catch (e) {
|
||||
logger.error({ message: `Failed to update stats for faculty ${code} with type ALL`, meta: `${e}` })
|
||||
}
|
||||
await updateFacultyOverview(code, 'ALL')
|
||||
await updateFacultyProgressOverview(code)
|
||||
}
|
||||
|
||||
const refreshProgrammes = async () => {
|
||||
logger.info('Refreshing studyprogramme and studytrack overview statistics for all programmes')
|
||||
// Filters out old programmes and special ones like Fitech studies. Filters also out the programmes starting with
|
||||
// 2_ or ending with _2. Those programmes are mapped to correct programme (studentProgrammeModuleFixer.js)
|
||||
const codes = (await getAllProgrammes())
|
||||
.map(p => p.code)
|
||||
.filter(
|
||||
code =>
|
||||
(code.includes('KH') && !code.startsWith('2_KH') && !code.endsWith('_2')) ||
|
||||
(code.includes('MH') && !code.startsWith('2_MH') && !code.endsWith('_2')) ||
|
||||
/^(T)[0-9]{6}$/.test(code)
|
||||
)
|
||||
|
||||
const programmes = await getProgrammesFromStudyRights()
|
||||
const codes = programmes.map(programme => programme.code).filter(code => isRelevantProgramme(code))
|
||||
|
||||
// Ensure that studyright associations are refreshed before launching jobs, otherwise each job will do it
|
||||
await getAssociations()
|
||||
@@ -69,13 +53,12 @@ const refreshProgrammes = async () => {
|
||||
}
|
||||
|
||||
const refreshProgramme = async code => {
|
||||
const associations = await getAssociations()
|
||||
await updateBasicView(code, '')
|
||||
await updateStudytrackView(code, '', associations)
|
||||
await updateStudytrackView(code, '')
|
||||
|
||||
const combinedProgramme = combinedStudyprogrammes[code] || ''
|
||||
await updateBasicView(code, combinedProgramme)
|
||||
await updateStudytrackView(code, combinedProgramme, associations)
|
||||
await updateStudytrackView(code, combinedProgramme)
|
||||
}
|
||||
|
||||
const refreshTeacherLeaderboard = async () => {
|
||||
@@ -112,7 +95,6 @@ const dailyJobs = () => {
|
||||
refreshProgrammes()
|
||||
jobMaker.languagecenter()
|
||||
jobMaker.statistics()
|
||||
refreshCloseToGraduating()
|
||||
}
|
||||
|
||||
const startCron = () => {
|
||||
@@ -122,10 +104,14 @@ const startCron = () => {
|
||||
logger.info('Running daily jobs from cron')
|
||||
dailyJobs()
|
||||
})
|
||||
// This needs to be its own job because refreshing study right associations in `refreshStatistics` causes some study right info to be not available, causing this job to fail
|
||||
schedule('0 3 * * *', async () => {
|
||||
jobMaker.closeToGraduation()
|
||||
})
|
||||
schedule('0 4 * * 3', async () => {
|
||||
logger.info("Deleting users who haven't logged in for 18 months")
|
||||
const numberOfDeletedUsers = await deleteOutdatedUsers()
|
||||
logger.info(`Deleted ${numberOfDeletedUsers} users.`)
|
||||
const [, result] = await deleteOutdatedUsers()
|
||||
logger.info(`Deleted ${result.rowCount} users.`)
|
||||
})
|
||||
schedule('0 19 * * 1', async () => {
|
||||
logger.info('Updating students whose studyplans have not been updated recently')
|
||||
@@ -146,4 +132,5 @@ module.exports = {
|
||||
refreshProgramme,
|
||||
refreshProgrammes,
|
||||
refreshLanguageCenterData,
|
||||
refreshCloseToGraduating,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const { omit } = require('lodash')
|
||||
const morgan = require('morgan')
|
||||
|
||||
const { getFullStudyProgrammeRights } = require('../util')
|
||||
const logger = require('../util/logger')
|
||||
const _ = require('lodash')
|
||||
const { getFullStudyProgrammeRights } = require('../util/utils')
|
||||
|
||||
// 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!).
|
||||
@@ -24,17 +25,18 @@ const accessLogger = morgan((tokens, req, res) => {
|
||||
`${(tokens['response-time'](req, res) || '0').split('.')[0]} ms`.padEnd(8, ' '),
|
||||
tokens.status(req, res),
|
||||
tokens.method(req, res),
|
||||
tokens.url(req, res),
|
||||
decodeURIComponent(tokens.url(req, res)),
|
||||
].join(' ')
|
||||
|
||||
const fullStudyProgrammeRights = getFullStudyProgrammeRights(user.programmeRights)
|
||||
const iamBasedRights = user.programmeRights.filter(right => right.isIamBased)
|
||||
|
||||
const usingIamRights = user.iamRights.some(programmeCode => tokens.url(req, res).includes(programmeCode))
|
||||
const usingIamRights = iamBasedRights.some(right => tokens.url(req, res).includes(right.code))
|
||||
const onlyIamRights = !user.isAdmin && fullStudyProgrammeRights.length === 0
|
||||
|
||||
logger.info(message, {
|
||||
// don't log student list which might be huge
|
||||
..._.omit(meta, ['studentsUserCanAccess']),
|
||||
...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
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
const Sentry = require('@sentry/node')
|
||||
const { ApplicationError } = require('../util/customErrors')
|
||||
const { intersection } = require('lodash')
|
||||
|
||||
const { requiredGroup, serviceProvider, configLogoutUrl, isDev } = require('../config')
|
||||
const { getUser, getMockedUser, getOrganizationAccess } = require('../services/userService')
|
||||
const { requiredGroup } = require('../conf-backend')
|
||||
const _ = require('lodash')
|
||||
const { ApplicationError } = require('../util/customErrors')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const parseIamGroups = iamGroups => iamGroups?.split(';').filter(Boolean) ?? []
|
||||
const parseIamGroups = iamGroups => iamGroups?.split(';') ?? []
|
||||
|
||||
const hasRequiredIamGroup = (iamGroups, iamRights) =>
|
||||
_.intersection(iamGroups, requiredGroup).length > 0 || iamRights.length > 0
|
||||
const hasRequiredIamGroup = (iamGroups, iamRights) => {
|
||||
return intersection(iamGroups, requiredGroup).length > 0 || iamRights.length > 0
|
||||
}
|
||||
|
||||
const currentUserMiddleware = async (req, _res, next) => {
|
||||
const toskaUserMiddleware = async (req, _res, next) => {
|
||||
const {
|
||||
'shib-session-id': sessionId,
|
||||
'x-show-as-user': showAsUser,
|
||||
@@ -36,27 +38,23 @@ const currentUserMiddleware = async (req, _res, next) => {
|
||||
}
|
||||
|
||||
const iamGroups = parseIamGroups(hygroupcn)
|
||||
const { access = {}, specialGroup = {} } = await getOrganizationAccess({ id: sisId, iamGroups })
|
||||
const { access = {}, specialGroup = {} } = await getOrganizationAccess(sisId, iamGroups)
|
||||
const iamRights = Object.keys(access)
|
||||
|
||||
if (!hasRequiredIamGroup(iamGroups, iamRights)) {
|
||||
logger.error({ message: 'User does not have required iam group', meta: { iamGroups, iamRights } })
|
||||
logger.error({
|
||||
message: 'User does not have required iam group',
|
||||
meta: { username, name, email, iamGroups, iamRights },
|
||||
})
|
||||
throw new ApplicationError(`User '${username}' does not have required iam group`, 403, { logoutUrl })
|
||||
}
|
||||
|
||||
let user = await getUser({
|
||||
username,
|
||||
name,
|
||||
email,
|
||||
iamGroups,
|
||||
iamRights,
|
||||
specialGroup,
|
||||
sisId,
|
||||
access,
|
||||
})
|
||||
let user
|
||||
|
||||
if (showAsUser && user.isAdmin) {
|
||||
if (showAsUser && specialGroup?.superAdmin) {
|
||||
user = await getMockedUser({ userToMock: showAsUser, mockedBy: username })
|
||||
} else {
|
||||
user = await getUser({ username, name, email, iamGroups, specialGroup, sisId, access })
|
||||
}
|
||||
|
||||
Sentry.setUser({ username: user.mockedBy ?? username })
|
||||
@@ -67,4 +65,32 @@ const currentUserMiddleware = async (req, _res, next) => {
|
||||
next()
|
||||
}
|
||||
|
||||
const fdUserMiddleware = async (req, _res, next) => {
|
||||
const { remote_user: remoteUser, 'x-show-as-user': showAsUser } = req.headers
|
||||
|
||||
if (!remoteUser) {
|
||||
throw new ApplicationError('Not enough data in request headers, remote_user was missing', 403, { configLogoutUrl })
|
||||
}
|
||||
|
||||
let user
|
||||
|
||||
// getMockedUser in production requires the superAdmin-role, which is only available via iamGroups, so it's now only implemented for the dev environment
|
||||
if (showAsUser && isDev) {
|
||||
user = await getMockedUser({ userToMock: showAsUser, mockedBy: remoteUser })
|
||||
} else {
|
||||
user = await getUser({ username: remoteUser })
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new ApplicationError(`Could not grant access with the eppn ${remoteUser}.`, 403, { configLogoutUrl })
|
||||
}
|
||||
|
||||
req.user = user
|
||||
req.logoutUrl = configLogoutUrl
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
const currentUserMiddleware = serviceProvider === 'Toska' ? toskaUserMiddleware : fdUserMiddleware
|
||||
|
||||
module.exports = currentUserMiddleware
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
const Sentry = require('@sentry/node')
|
||||
|
||||
const { ApplicationError } = require('../util/customErrors')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const errorHandler = (error, _req, res, next) => {
|
||||
logger.error(`${error.message} ${error.name} ${error.stack}`)
|
||||
logger.error(error.message, { error })
|
||||
|
||||
Sentry.captureException(error)
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
const headersMiddleware = require('unfuck-utf8-headers-middleware')
|
||||
|
||||
const headers = ['uid', 'displayName', 'shib-session-id', 'hyGroupCn', 'mail', 'hyPersonSisuId']
|
||||
const serviceProvider = require('../config')
|
||||
|
||||
const headers =
|
||||
serviceProvider === 'Toska'
|
||||
? ['uid', 'displayName', 'shib-session-id', 'hyGroupCn', 'mail', 'hyPersonSisuId']
|
||||
: ['remote_user']
|
||||
|
||||
module.exports = headersMiddleware(headers)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
Column,
|
||||
CreatedAt,
|
||||
DataType,
|
||||
ForeignKey,
|
||||
HasMany,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
Table,
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import { EnrollmentType, ExtentCode } from '../types'
|
||||
import { Organization } from './organization'
|
||||
import { SISStudyRightElement } from './SISStudyRightElement'
|
||||
import { Student } from './student'
|
||||
import { Studyplan } from './studyplan'
|
||||
import { StudyrightExtent } from './studyrightExtent'
|
||||
|
||||
export type SemesterEnrollment = {
|
||||
type: EnrollmentType
|
||||
semester: number
|
||||
statutoryAbsence?: boolean
|
||||
}
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
tableName: 'sis_study_rights',
|
||||
})
|
||||
export class SISStudyRight extends Model<InferAttributes<SISStudyRight>> {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
|
||||
@ForeignKey(() => Organization)
|
||||
@Column(DataType.STRING)
|
||||
facultyCode!: string
|
||||
|
||||
@BelongsTo(() => Organization, { foreignKey: 'facultyCode', targetKey: 'code' })
|
||||
organization!: Organization
|
||||
|
||||
@HasMany(() => Studyplan)
|
||||
studyPlans!: Studyplan[]
|
||||
|
||||
@HasMany(() => SISStudyRightElement, { foreignKey: 'studyRightId' })
|
||||
studyRightElements!: SISStudyRightElement[]
|
||||
|
||||
@Column(DataType.DATE)
|
||||
startDate!: Date
|
||||
|
||||
@Column(DataType.DATE)
|
||||
endDate!: Date
|
||||
|
||||
@Column(DataType.DATE)
|
||||
studyStartDate!: Date
|
||||
|
||||
@Column(DataType.BOOLEAN)
|
||||
cancelled!: boolean
|
||||
|
||||
@ForeignKey(() => Student)
|
||||
@Column(DataType.STRING)
|
||||
studentNumber!: string
|
||||
|
||||
@BelongsTo(() => Student)
|
||||
student!: Student
|
||||
|
||||
@ForeignKey(() => StudyrightExtent)
|
||||
@Column(DataType.INTEGER)
|
||||
extentCode!: ExtentCode
|
||||
|
||||
@Column(DataType.STRING)
|
||||
admissionType!: string
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
semesterEnrollments!: SemesterEnrollment[] | null
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
Column,
|
||||
CreatedAt,
|
||||
DataType,
|
||||
ForeignKey,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
Table,
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import { Name, Phase, StudyTrack } from '../types'
|
||||
import { SISStudyRight } from './SISStudyRight'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
tableName: 'sis_study_right_elements',
|
||||
})
|
||||
export class SISStudyRightElement extends Model<InferAttributes<SISStudyRightElement>> {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
|
||||
@Column(DataType.DATE)
|
||||
startDate!: Date
|
||||
|
||||
@Column(DataType.DATE)
|
||||
endDate!: Date
|
||||
|
||||
@Column(DataType.BOOLEAN)
|
||||
graduated!: boolean
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
phase!: Phase
|
||||
|
||||
@ForeignKey(() => SISStudyRight)
|
||||
@Column(DataType.STRING)
|
||||
studyRightId!: string
|
||||
|
||||
@BelongsTo(() => SISStudyRight, { foreignKey: 'studyRightId' })
|
||||
studyRight!: SISStudyRight
|
||||
|
||||
@Column(DataType.STRING)
|
||||
code!: string
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
name!: Name
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
studyTrack!: StudyTrack | null
|
||||
|
||||
@Column(DataType.STRING)
|
||||
degreeProgrammeType!: string
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
const { Model, STRING, DATE, JSONB, BOOLEAN } = require('sequelize')
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
class Course extends Model {}
|
||||
|
||||
Course.init(
|
||||
{
|
||||
id: {
|
||||
type: STRING,
|
||||
primaryKey: true,
|
||||
},
|
||||
code: {
|
||||
type: STRING,
|
||||
},
|
||||
name: {
|
||||
type: JSONB,
|
||||
},
|
||||
latest_instance_date: {
|
||||
type: DATE,
|
||||
},
|
||||
is_study_module: {
|
||||
type: BOOLEAN,
|
||||
},
|
||||
coursetypecode: {
|
||||
type: STRING,
|
||||
},
|
||||
startdate: {
|
||||
type: DATE,
|
||||
},
|
||||
enddate: {
|
||||
type: DATE,
|
||||
},
|
||||
max_attainment_date: {
|
||||
type: DATE,
|
||||
},
|
||||
min_attainment_date: {
|
||||
type: DATE,
|
||||
},
|
||||
createdAt: {
|
||||
field: 'created_at',
|
||||
type: DATE,
|
||||
},
|
||||
updatedAt: {
|
||||
field: 'updated_at',
|
||||
type: DATE,
|
||||
},
|
||||
substitutions: {
|
||||
type: JSONB,
|
||||
},
|
||||
course_unit_type: {
|
||||
type: STRING,
|
||||
},
|
||||
mainCourseCode: {
|
||||
field: 'main_course_code',
|
||||
type: STRING,
|
||||
},
|
||||
},
|
||||
{
|
||||
underscored: false,
|
||||
sequelize: dbConnections.sequelize,
|
||||
modelName: 'course',
|
||||
tableName: 'course',
|
||||
}
|
||||
)
|
||||
|
||||
module.exports = Course
|
||||
@@ -0,0 +1,100 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
BelongsToMany,
|
||||
Column,
|
||||
CreatedAt,
|
||||
DataType,
|
||||
ForeignKey,
|
||||
HasMany,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
Table,
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import { Name } from '../types'
|
||||
import { CourseProvider } from './courseProvider'
|
||||
import { CourseType } from './courseType'
|
||||
import { Credit } from './credit'
|
||||
import { Enrollment } from './enrollment'
|
||||
import { Organization } from './organization'
|
||||
|
||||
@Table({
|
||||
underscored: false,
|
||||
modelName: 'course',
|
||||
tableName: 'course',
|
||||
})
|
||||
export class Course extends Model<InferAttributes<Course>> {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
|
||||
@ForeignKey(() => Course)
|
||||
@Column(DataType.STRING)
|
||||
code!: string
|
||||
|
||||
@ForeignKey(() => CourseType)
|
||||
@Column(DataType.STRING)
|
||||
coursetypecode!: string
|
||||
|
||||
@BelongsTo(() => CourseType, { foreignKey: 'coursetypecode', targetKey: 'coursetypecode' })
|
||||
courseType!: CourseType
|
||||
|
||||
@HasMany(() => Credit, { foreignKey: 'course_id', sourceKey: 'id' })
|
||||
credits!: Credit[]
|
||||
|
||||
@HasMany(() => Enrollment, { foreignKey: 'course_id' })
|
||||
enrollments!: Enrollment[]
|
||||
|
||||
@BelongsToMany(() => Organization, () => CourseProvider, 'coursecode')
|
||||
organizations!: Organization[]
|
||||
|
||||
@Column(DataType.BOOLEAN)
|
||||
is_study_module!: boolean
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
name!: Name
|
||||
|
||||
@Column(DataType.DATE)
|
||||
latest_instance_date!: Date
|
||||
|
||||
@Column(DataType.DATE)
|
||||
startdate!: Date
|
||||
|
||||
@Column(DataType.DATE)
|
||||
enddate!: Date
|
||||
|
||||
@Column(DataType.DATE)
|
||||
max_attainment_date!: Date
|
||||
|
||||
@Column(DataType.DATE)
|
||||
min_attainment_date!: Date
|
||||
|
||||
@CreatedAt
|
||||
@Column({
|
||||
field: 'created_at',
|
||||
type: DataType.DATE,
|
||||
})
|
||||
createdAt!: Date
|
||||
|
||||
@UpdatedAt
|
||||
@Column({
|
||||
field: 'updated_at',
|
||||
type: DataType.DATE,
|
||||
})
|
||||
updatedAt!: Date
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
substitutions!: string[]
|
||||
|
||||
@Column(DataType.STRING)
|
||||
course_unit_type!: string
|
||||
|
||||
@Column({
|
||||
field: 'main_course_code',
|
||||
type: DataType.STRING,
|
||||
})
|
||||
mainCourseCode!: string
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
const { Model, STRING, DATE, JSONB } = require('sequelize')
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
class CourseProvider extends Model {}
|
||||
|
||||
CourseProvider.init(
|
||||
{
|
||||
composite: {
|
||||
type: STRING,
|
||||
unique: true,
|
||||
},
|
||||
coursecode: {
|
||||
type: STRING,
|
||||
references: {
|
||||
model: 'course',
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'cascade',
|
||||
onDelete: 'cascade',
|
||||
},
|
||||
shares: {
|
||||
type: JSONB,
|
||||
},
|
||||
organizationcode: {
|
||||
type: STRING,
|
||||
references: {
|
||||
model: 'organization',
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'cascade',
|
||||
onDelete: 'cascade',
|
||||
},
|
||||
createdAt: {
|
||||
type: DATE,
|
||||
},
|
||||
updatedAt: {
|
||||
type: DATE,
|
||||
},
|
||||
},
|
||||
{
|
||||
underscored: true,
|
||||
sequelize: dbConnections.sequelize,
|
||||
modelName: 'course_provider',
|
||||
tableName: 'course_providers',
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['coursecode', 'organizationcode'],
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
module.exports = CourseProvider
|
||||
@@ -0,0 +1,42 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, ForeignKey, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { Course } from './course'
|
||||
import { Organization } from './organization'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
modelName: 'course_provider',
|
||||
tableName: 'course_providers',
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['coursecode', 'organizationcode'],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class CourseProvider extends Model<InferAttributes<CourseProvider>> {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
composite!: string
|
||||
|
||||
@ForeignKey(() => Course)
|
||||
@Column(DataType.STRING)
|
||||
coursecode!: string
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
shares!: Array<{ share: number; startDate?: string; endDate?: string }> | null
|
||||
|
||||
@ForeignKey(() => Organization)
|
||||
@Column(DataType.STRING)
|
||||
organizationcode!: string
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
const { Model, STRING, DATE, JSONB } = require('sequelize')
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
class CourseType extends Model {}
|
||||
|
||||
CourseType.init(
|
||||
{
|
||||
coursetypecode: {
|
||||
type: STRING,
|
||||
primaryKey: true,
|
||||
},
|
||||
name: {
|
||||
type: JSONB,
|
||||
},
|
||||
createdAt: {
|
||||
type: DATE,
|
||||
},
|
||||
updatedAt: {
|
||||
type: DATE,
|
||||
},
|
||||
},
|
||||
{
|
||||
underscored: true,
|
||||
sequelize: dbConnections.sequelize,
|
||||
modelName: 'course_type',
|
||||
tableName: 'course_types',
|
||||
}
|
||||
)
|
||||
|
||||
module.exports = CourseType
|
||||
@@ -0,0 +1,31 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, HasMany, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { Name } from '../types'
|
||||
import { Course } from './course'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
modelName: 'course_type',
|
||||
tableName: 'course_types',
|
||||
})
|
||||
export class CourseType extends Model<InferAttributes<CourseType>> {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
coursetypecode!: string
|
||||
|
||||
@HasMany(() => Course)
|
||||
courses!: Course[]
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
name!: Required<Name>
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
const { Model, DATE, INTEGER, STRING, DOUBLE, BOOLEAN } = require('sequelize')
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
class Credit extends Model {}
|
||||
|
||||
Credit.init(
|
||||
{
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: STRING,
|
||||
},
|
||||
grade: {
|
||||
type: STRING,
|
||||
},
|
||||
student_studentnumber: {
|
||||
type: STRING,
|
||||
references: {
|
||||
model: 'student',
|
||||
key: 'studentnumber',
|
||||
},
|
||||
onUpdate: 'cascade',
|
||||
onDelete: 'cascade',
|
||||
},
|
||||
credits: {
|
||||
type: DOUBLE,
|
||||
},
|
||||
createdate: {
|
||||
type: DATE,
|
||||
},
|
||||
credittypecode: {
|
||||
type: INTEGER,
|
||||
references: {
|
||||
model: 'credit_types',
|
||||
key: 'credittypecode',
|
||||
},
|
||||
onUpdate: 'cascade',
|
||||
onDelete: 'cascade',
|
||||
},
|
||||
attainment_date: {
|
||||
type: DATE,
|
||||
},
|
||||
course_code: {
|
||||
type: STRING,
|
||||
},
|
||||
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,
|
||||
},
|
||||
isStudyModule: {
|
||||
type: BOOLEAN,
|
||||
},
|
||||
org: {
|
||||
type: STRING,
|
||||
},
|
||||
createdAt: {
|
||||
type: DATE,
|
||||
},
|
||||
updatedAt: {
|
||||
type: DATE,
|
||||
},
|
||||
language: {
|
||||
type: STRING,
|
||||
},
|
||||
is_open: {
|
||||
type: BOOLEAN,
|
||||
},
|
||||
studyright_id: {
|
||||
type: STRING,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'studyright',
|
||||
key: 'studyrightid',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
underscored: false,
|
||||
sequelize: dbConnections.sequelize,
|
||||
modelName: 'credit',
|
||||
tableName: 'credit',
|
||||
}
|
||||
)
|
||||
|
||||
module.exports = Credit
|
||||
@@ -0,0 +1,117 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
BelongsToMany,
|
||||
Column,
|
||||
CreatedAt,
|
||||
DataType,
|
||||
ForeignKey,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
Table,
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import { CreditTypeCode } from '../types'
|
||||
import { Course } from './course'
|
||||
import { CreditTeacher } from './creditTeacher'
|
||||
import { CreditType } from './creditType'
|
||||
import { Semester } from './semester'
|
||||
import { Student } from './student'
|
||||
import { Studyright } from './studyright'
|
||||
import { Teacher } from './teacher'
|
||||
|
||||
@Table({
|
||||
underscored: false,
|
||||
modelName: 'credit',
|
||||
tableName: 'credit',
|
||||
})
|
||||
export class Credit extends Model<InferAttributes<Credit>> {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
|
||||
@Column(DataType.STRING)
|
||||
grade!: string
|
||||
|
||||
@ForeignKey(() => Student)
|
||||
@Column(DataType.STRING)
|
||||
student_studentnumber!: string
|
||||
|
||||
@BelongsTo(() => Student, { foreignKey: 'student_studentnumber', targetKey: 'studentnumber' })
|
||||
student!: Student
|
||||
|
||||
@BelongsTo(() => Semester, { foreignKey: 'semester_composite' })
|
||||
semester!: Semester
|
||||
|
||||
@Column(DataType.DOUBLE)
|
||||
credits!: number
|
||||
|
||||
@ForeignKey(() => CreditType)
|
||||
@Column(DataType.INTEGER)
|
||||
credittypecode!: CreditTypeCode
|
||||
|
||||
@BelongsTo(() => CreditType, { foreignKey: 'credittypecode', targetKey: 'credittypecode' })
|
||||
creditType!: CreditType
|
||||
|
||||
@Column(DataType.DATE)
|
||||
createdate!: Date
|
||||
|
||||
@Column(DataType.DATE)
|
||||
attainment_date!: Date
|
||||
|
||||
@BelongsToMany(() => Teacher, () => CreditTeacher, 'credit_id')
|
||||
teachers!: Teacher[]
|
||||
|
||||
@Column(DataType.STRING)
|
||||
course_code!: string
|
||||
|
||||
@BelongsTo(() => Course, { foreignKey: 'course_id' })
|
||||
course!: Course
|
||||
|
||||
@ForeignKey(() => Course)
|
||||
@Column(DataType.STRING)
|
||||
course_id!: string
|
||||
|
||||
@Column(DataType.STRING)
|
||||
semester_composite!: string
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
semestercode!: number
|
||||
|
||||
@Column(DataType.BOOLEAN)
|
||||
isStudyModule!: boolean
|
||||
|
||||
@Column(DataType.STRING)
|
||||
org!: string
|
||||
|
||||
@Column(DataType.STRING)
|
||||
language!: string
|
||||
|
||||
@Column(DataType.BOOLEAN)
|
||||
is_open!: boolean
|
||||
|
||||
@ForeignKey(() => Studyright)
|
||||
@Column(DataType.STRING)
|
||||
studyright_id!: string
|
||||
|
||||
@BelongsTo(() => Studyright)
|
||||
studyright!: Studyright
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
|
||||
static passed: ({ credittypecode }: { credittypecode: CreditTypeCode }) => boolean
|
||||
|
||||
static failed: ({ credittypecode }: { credittypecode: CreditTypeCode }) => boolean
|
||||
|
||||
static improved: ({ credittypecode }: { credittypecode: CreditTypeCode }) => boolean
|
||||
|
||||
static notUnnecessary: ({ credits }: { credits: number }) => boolean
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
const { Model, STRING, DATE } = require('sequelize')
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
class CreditTeacher extends Model {}
|
||||
|
||||
CreditTeacher.init(
|
||||
{
|
||||
composite: {
|
||||
type: STRING,
|
||||
unique: true,
|
||||
},
|
||||
credit_id: {
|
||||
type: STRING,
|
||||
references: {
|
||||
model: 'credit',
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'cascade',
|
||||
onDelete: 'cascade',
|
||||
},
|
||||
teacher_id: {
|
||||
type: STRING,
|
||||
references: {
|
||||
model: 'teacher',
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'cascade',
|
||||
onDelete: 'cascade',
|
||||
},
|
||||
createdAt: {
|
||||
type: DATE,
|
||||
},
|
||||
updatedAt: {
|
||||
type: DATE,
|
||||
},
|
||||
},
|
||||
{
|
||||
underscored: false,
|
||||
sequelize: dbConnections.sequelize,
|
||||
modelName: 'credit_teacher',
|
||||
tableName: 'credit_teachers',
|
||||
}
|
||||
)
|
||||
|
||||
module.exports = CreditTeacher
|
||||
@@ -0,0 +1,44 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
Column,
|
||||
CreatedAt,
|
||||
DataType,
|
||||
ForeignKey,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
Table,
|
||||
Unique,
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import { Credit } from './credit'
|
||||
import { Teacher } from './teacher'
|
||||
|
||||
@Table({
|
||||
underscored: false,
|
||||
modelName: 'credit_teacher',
|
||||
tableName: 'credit_teachers',
|
||||
})
|
||||
export class CreditTeacher extends Model<InferAttributes<CreditTeacher>> {
|
||||
@PrimaryKey
|
||||
@Unique
|
||||
@Column(DataType.STRING)
|
||||
composite!: string
|
||||
|
||||
@ForeignKey(() => Credit)
|
||||
@Column(DataType.STRING)
|
||||
credit_id!: string
|
||||
|
||||
@ForeignKey(() => Teacher)
|
||||
@Column(DataType.STRING)
|
||||
teacher_id!: string
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
const { Model, INTEGER, DATE, JSONB } = require('sequelize')
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
class CreditType extends Model {}
|
||||
|
||||
CreditType.init(
|
||||
{
|
||||
credittypecode: {
|
||||
primaryKey: true,
|
||||
type: INTEGER,
|
||||
},
|
||||
name: {
|
||||
type: JSONB,
|
||||
},
|
||||
createdAt: {
|
||||
type: DATE,
|
||||
},
|
||||
updatedAt: {
|
||||
type: DATE,
|
||||
},
|
||||
},
|
||||
{
|
||||
underscored: true,
|
||||
sequelize: dbConnections.sequelize,
|
||||
modelName: 'credit_type',
|
||||
tableName: 'credit_types',
|
||||
}
|
||||
)
|
||||
|
||||
module.exports = CreditType
|
||||
@@ -0,0 +1,26 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { CreditTypeCode, Name } from '../types'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
modelName: 'credit_type',
|
||||
tableName: 'credit_types',
|
||||
})
|
||||
export class CreditType extends Model<InferAttributes<CreditType>> {
|
||||
@PrimaryKey
|
||||
@Column(DataType.INTEGER)
|
||||
credittypecode!: CreditTypeCode
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
name!: Required<Name>
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
const { Model, INTEGER, DATE, JSONB, STRING } = require('sequelize')
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
class ElementDetail extends Model {}
|
||||
|
||||
ElementDetail.init(
|
||||
{
|
||||
code: { type: STRING, primaryKey: true },
|
||||
name: { type: JSONB },
|
||||
type: { type: INTEGER },
|
||||
createdAt: {
|
||||
type: DATE,
|
||||
},
|
||||
updatedAt: {
|
||||
type: DATE,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize: dbConnections.sequelize,
|
||||
modelName: 'element_detail',
|
||||
tableName: 'element_details',
|
||||
}
|
||||
)
|
||||
|
||||
module.exports = ElementDetail
|
||||
@@ -0,0 +1,34 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, HasMany, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { ElementDetailType, Name } from '../types'
|
||||
import { StudyrightElement } from './studyrightElement'
|
||||
|
||||
@Table({
|
||||
underscored: false,
|
||||
modelName: 'element_detail',
|
||||
tableName: 'element_details',
|
||||
})
|
||||
export class ElementDetail extends Model<InferAttributes<ElementDetail>> {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
code!: string
|
||||
|
||||
@Column(DataType.JSONB)
|
||||
name!: Name
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
type!: ElementDetailType
|
||||
|
||||
@HasMany(() => StudyrightElement, { foreignKey: 'code', sourceKey: 'code' })
|
||||
studyrightElements!: StudyrightElement
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
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,
|
||||
},
|
||||
studyright_id: {
|
||||
type: STRING,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'studyright',
|
||||
key: 'studyrightid',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
underscored: false,
|
||||
sequelize: dbConnections.sequelize,
|
||||
modelName: 'enrollment',
|
||||
tableName: 'enrollment',
|
||||
}
|
||||
)
|
||||
|
||||
module.exports = Enrollment
|
||||
@@ -0,0 +1,80 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
Column,
|
||||
CreatedAt,
|
||||
DataType,
|
||||
ForeignKey,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
Table,
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import { Course } from './course'
|
||||
import { Semester } from './semester'
|
||||
import { Student } from './student'
|
||||
import { Studyright } from './studyright'
|
||||
|
||||
@Table({
|
||||
underscored: false,
|
||||
modelName: 'enrollment',
|
||||
tableName: 'enrollment',
|
||||
})
|
||||
export class Enrollment extends Model<InferAttributes<Enrollment>> {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
id!: string
|
||||
|
||||
@ForeignKey(() => Student)
|
||||
@Column(DataType.STRING)
|
||||
studentnumber!: string
|
||||
|
||||
@BelongsTo(() => Student, { foreignKey: 'studentnumber', targetKey: 'studentnumber' })
|
||||
student!: Student
|
||||
|
||||
@Column(DataType.STRING)
|
||||
course_code!: string
|
||||
|
||||
@Column(DataType.STRING)
|
||||
state!: string
|
||||
|
||||
@Column(DataType.DATE)
|
||||
enrollment_date_time!: Date
|
||||
|
||||
@ForeignKey(() => Course)
|
||||
@Column(DataType.STRING)
|
||||
course_id!: string
|
||||
|
||||
@ForeignKey(() => Semester)
|
||||
@Column(DataType.STRING)
|
||||
semester_composite!: string
|
||||
|
||||
@BelongsTo(() => Semester, { foreignKey: 'semester_composite', targetKey: 'composite' })
|
||||
semester!: Semester
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
semestercode!: number
|
||||
|
||||
@Column(DataType.BOOLEAN)
|
||||
is_open!: boolean
|
||||
|
||||
@ForeignKey(() => Studyright)
|
||||
@Column({ type: DataType.STRING, allowNull: true })
|
||||
studyright_id!: string
|
||||
|
||||
@BelongsTo(() => Studyright, { foreignKey: 'studyright_id', targetKey: 'studyrightid', constraints: false })
|
||||
studyright!: Studyright
|
||||
|
||||
@BelongsTo(() => Course, { foreignKey: 'course_id', targetKey: 'id' })
|
||||
course!: Course
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
|
||||
@UpdatedAt
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
const { Model, INTEGER, STRING, DATE } = require('sequelize')
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
class ExcludedCourse extends Model {}
|
||||
|
||||
ExcludedCourse.init(
|
||||
{
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: INTEGER,
|
||||
autoIncrement: true,
|
||||
},
|
||||
programme_code: {
|
||||
type: STRING,
|
||||
},
|
||||
course_code: {
|
||||
type: STRING,
|
||||
},
|
||||
curriculum_version: {
|
||||
type: STRING,
|
||||
},
|
||||
created_at: {
|
||||
type: DATE,
|
||||
},
|
||||
updated_at: {
|
||||
type: DATE,
|
||||
},
|
||||
},
|
||||
{
|
||||
underscored: true,
|
||||
sequelize: dbConnections.sequelize,
|
||||
modelName: 'excluded_course',
|
||||
tableName: 'excluded_courses',
|
||||
}
|
||||
)
|
||||
|
||||
module.exports = ExcludedCourse
|
||||
@@ -1,145 +0,0 @@
|
||||
const Organization = require('./organization')
|
||||
const CourseType = require('./courseType')
|
||||
const Course = require('./course')
|
||||
const Student = require('./student')
|
||||
const CourseProvider = require('./courseProvider')
|
||||
const Semester = require('./semester')
|
||||
const SemesterEnrollment = require('./semesterEnrollment')
|
||||
const Teacher = require('./teacher')
|
||||
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')
|
||||
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' })
|
||||
|
||||
Course.belongsToMany(Organization, { through: CourseProvider, foreignKey: 'coursecode' })
|
||||
Organization.belongsToMany(Course, { through: CourseProvider, foreignKey: 'organizationcode' })
|
||||
|
||||
SemesterEnrollment.belongsTo(Student, { foreignKey: 'studentnumber', targetKey: 'studentnumber' })
|
||||
Student.hasMany(SemesterEnrollment, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
|
||||
|
||||
SemesterEnrollment.belongsTo(Semester, { foreignKey: 'semestercomposite', targetKey: 'composite' })
|
||||
Semester.hasMany(SemesterEnrollment, { foreignKey: 'semestercomposite', sourceKey: 'composite' })
|
||||
|
||||
ProgrammeModule.belongsTo(Organization, { foreignKey: 'organization_id' })
|
||||
Organization.hasMany(ProgrammeModule, { foreignKey: 'organization_id' })
|
||||
|
||||
Credit.notUnnecessary = credit => {
|
||||
return credit.credits > 0 && credit.credits <= 12
|
||||
}
|
||||
|
||||
const CREDIT_TYPE_CODES = {
|
||||
PASSED: 4,
|
||||
FAILED: 10,
|
||||
IMPROVED: 7,
|
||||
APPROVED: 9,
|
||||
}
|
||||
|
||||
Credit.passed = ({ credittypecode }) =>
|
||||
credittypecode === CREDIT_TYPE_CODES.PASSED || credittypecode === CREDIT_TYPE_CODES.APPROVED
|
||||
Credit.failed = credit => credit.credittypecode === CREDIT_TYPE_CODES.FAILED
|
||||
Credit.improved = credit => credit.credittypecode === CREDIT_TYPE_CODES.IMPROVED
|
||||
Credit.belongsTo(Studyright, { foreignKey: 'studyright_id', targetKey: 'studyrightid', constraints: false })
|
||||
Studyright.hasMany(Credit, { foreignKey: 'studyright_id', constraints: false })
|
||||
|
||||
Credit.belongsTo(Student, { foreignKey: 'student_studentnumber', targetKey: 'studentnumber' })
|
||||
Student.hasMany(Credit, { foreignKey: 'student_studentnumber', sourceKey: 'studentnumber' })
|
||||
|
||||
Credit.belongsTo(Course, { foreignKey: 'course_id' })
|
||||
Course.hasMany(Credit, { foreignKey: 'course_id' })
|
||||
|
||||
Credit.belongsTo(CreditType, { foreignKey: 'credittypecode', targetKey: 'credittypecode' })
|
||||
Credit.belongsToMany(Teacher, { through: CreditTeacher, foreignKey: 'credit_id' })
|
||||
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' })
|
||||
|
||||
Studyplan.belongsTo(Studyright, { foreignKey: 'studyrightid', targetKey: 'studyrightid' })
|
||||
Studyright.hasMany(Studyplan, { foreignKey: 'studyrightid', sourceKey: 'studyrightid' })
|
||||
|
||||
Studyright.belongsTo(Student, { foreignKey: 'studentStudentnumber', targetKey: 'studentnumber' })
|
||||
Student.hasMany(Studyright, { foreignKey: 'studentStudentnumber', sourceKey: 'studentnumber' })
|
||||
|
||||
StudyrightElement.belongsTo(Studyright, { foreignKey: 'studyrightid', targetKey: 'studyrightid' })
|
||||
Studyright.hasMany(StudyrightElement, { foreignKey: 'studyrightid', sourceKey: 'studyrightid' })
|
||||
|
||||
StudyrightElement.belongsTo(ElementDetail, { foreignKey: 'code', targetKey: 'code' })
|
||||
ElementDetail.hasMany(StudyrightElement, { foreignKey: 'code', sourceKey: 'code' })
|
||||
|
||||
StudyrightElement.belongsTo(Student, { foreignKey: 'studentnumber', targetKey: 'studentnumber' })
|
||||
Student.hasMany(StudyrightElement, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
|
||||
|
||||
StudyrightExtent.hasMany(Studyright, { foreignKey: 'extentcode', sourceKey: 'extentcode' })
|
||||
Studyright.belongsTo(StudyrightExtent, { foreignKey: 'extentcode', targetKey: 'extentcode' })
|
||||
|
||||
Credit.belongsTo(Semester, { foreignKey: { name: 'semester_composite', allowNull: false } })
|
||||
|
||||
Transfer.belongsTo(Student, { foreignKey: 'studentnumber', targetKey: 'studentnumber' })
|
||||
Student.hasMany(Transfer, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
|
||||
|
||||
Transfer.belongsTo(Studyright, { foreignKey: 'studyrightid', targetKey: 'studyrightid' })
|
||||
Studyright.hasMany(Transfer, { foreignKey: 'studyrightid', sourceKey: 'studyrightid' })
|
||||
|
||||
Transfer.belongsTo(ElementDetail, { as: 'source', foreignKey: 'sourcecode' })
|
||||
Transfer.belongsTo(ElementDetail, { as: 'target', foreignKey: 'targetcode' })
|
||||
|
||||
Enrollment.belongsTo(Student, { foreignKey: 'studentnumber', targetKey: 'studentnumber' })
|
||||
Enrollment.belongsTo(Studyright, { foreignKey: 'studyright_id', targetKey: 'studyrightid', constraints: false })
|
||||
Studyright.hasMany(Enrollment, { foreignKey: 'studyright_id', constraints: false })
|
||||
|
||||
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,
|
||||
foreignKey: 'child_id',
|
||||
})
|
||||
ProgrammeModule.belongsToMany(ProgrammeModule, {
|
||||
as: 'children',
|
||||
through: ProgrammeModuleChild,
|
||||
foreignKey: 'parent_id',
|
||||
})
|
||||
|
||||
Organization.hasMany(Organization, { foreignKey: 'parent_id', as: 'children' })
|
||||
|
||||
module.exports = {
|
||||
Organization,
|
||||
Course,
|
||||
CourseType,
|
||||
Student,
|
||||
CourseProvider,
|
||||
Semester,
|
||||
SemesterEnrollment,
|
||||
Teacher,
|
||||
CreditType,
|
||||
Credit,
|
||||
CreditTeacher,
|
||||
ElementDetail,
|
||||
Studyplan,
|
||||
StudyrightExtent,
|
||||
Studyright,
|
||||
StudyrightElement,
|
||||
Transfer,
|
||||
ProgrammeModule,
|
||||
ProgrammeModuleChild,
|
||||
ExcludedCourse,
|
||||
Enrollment,
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { CreditTypeCode } from '../types'
|
||||
import { Course } from './course'
|
||||
import { CourseProvider } from './courseProvider'
|
||||
import { CourseType } from './courseType'
|
||||
import { Credit } from './credit'
|
||||
import { CreditTeacher } from './creditTeacher'
|
||||
import { CreditType } from './creditType'
|
||||
import { ElementDetail } from './elementDetail'
|
||||
import { Enrollment } from './enrollment'
|
||||
import { Organization } from './organization'
|
||||
import { ProgrammeModule } from './programmeModule'
|
||||
import { ProgrammeModuleChild } from './programmeModuleChild'
|
||||
import { Semester } from './semester'
|
||||
import { SemesterEnrollment } from './semesterEnrollment'
|
||||
import { SISStudyRight } from './SISStudyRight'
|
||||
import { SISStudyRightElement } from './SISStudyRightElement'
|
||||
import { Student } from './student'
|
||||
import { Studyplan } from './studyplan'
|
||||
import { Studyright } from './studyright'
|
||||
import { StudyrightElement } from './studyrightElement'
|
||||
import { StudyrightExtent } from './studyrightExtent'
|
||||
import { Teacher } from './teacher'
|
||||
import { Transfer } from './transfer'
|
||||
|
||||
Credit.passed = ({ credittypecode }) => [CreditTypeCode.PASSED, CreditTypeCode.APPROVED].includes(credittypecode)
|
||||
Credit.failed = ({ credittypecode }) => credittypecode === CreditTypeCode.FAILED
|
||||
Credit.improved = ({ credittypecode }) => credittypecode === CreditTypeCode.IMPROVED
|
||||
Credit.notUnnecessary = ({ credits }) => credits > 0 && credits <= 12
|
||||
|
||||
export {
|
||||
Course,
|
||||
CourseProvider,
|
||||
CourseType,
|
||||
Credit,
|
||||
CreditTeacher,
|
||||
CreditType,
|
||||
ElementDetail,
|
||||
Enrollment,
|
||||
Organization,
|
||||
ProgrammeModule,
|
||||
ProgrammeModuleChild,
|
||||
Semester,
|
||||
SemesterEnrollment,
|
||||
SISStudyRight,
|
||||
SISStudyRightElement,
|
||||
Student,
|
||||
Studyplan,
|
||||
Studyright,
|
||||
StudyrightExtent,
|
||||
StudyrightElement,
|
||||
Teacher,
|
||||
Transfer,
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize'
|
||||
import { AutoIncrement, Column, DataType, Model, PrimaryKey, Table } from 'sequelize-typescript'
|
||||
|
||||
@Table({
|
||||
underscored: false,
|
||||
modelName: 'custom_population_search',
|
||||
tableName: 'custom_population_searches',
|
||||
})
|
||||
export class CustomPopulationSearch extends Model<
|
||||
InferAttributes<CustomPopulationSearch>,
|
||||
InferCreationAttributes<CustomPopulationSearch>
|
||||
> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column(DataType.BIGINT)
|
||||
id!: CreationOptional<bigint>
|
||||
|
||||
@Column(DataType.BIGINT)
|
||||
userId!: bigint
|
||||
|
||||
@Column(DataType.STRING)
|
||||
name!: string
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
students!: string[]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, DataType, Model, PrimaryKey, Table } from 'sequelize-typescript'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
modelName: 'excluded_course',
|
||||
tableName: 'excluded_courses',
|
||||
})
|
||||
export class ExcludedCourse extends Model<InferAttributes<ExcludedCourse>> {
|
||||
@PrimaryKey
|
||||
@Column(DataType.INTEGER)
|
||||
id!: number
|
||||
|
||||
@Column(DataType.STRING)
|
||||
programme_code!: string
|
||||
|
||||
@Column(DataType.STRING)
|
||||
course_code!: string
|
||||
|
||||
@Column(DataType.STRING)
|
||||
curriculum_version!: string
|
||||
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
|
||||
@Column(DataType.DATE)
|
||||
updatedAt!: Date
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { CustomPopulationSearch } from './customPopulationSearch'
|
||||
import { ExcludedCourse } from './excludedCourse'
|
||||
import { OpenUniPopulationSearch } from './openUniPopulationSearch'
|
||||
import { ProgressCriteria } from './progressCriteria'
|
||||
import { StudyGuidanceGroupTag } from './studyGuidanceGroupTag'
|
||||
import { StudyProgrammePin } from './studyProgrammePin'
|
||||
import { Tag } from './tag'
|
||||
import { TagStudent } from './tagStudent'
|
||||
|
||||
export {
|
||||
CustomPopulationSearch,
|
||||
ExcludedCourse,
|
||||
OpenUniPopulationSearch,
|
||||
ProgressCriteria,
|
||||
StudyGuidanceGroupTag,
|
||||
StudyProgrammePin,
|
||||
Tag,
|
||||
TagStudent,
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize'
|
||||
import { AutoIncrement, Column, DataType, Model, PrimaryKey, Table } from 'sequelize-typescript'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
timestamps: true,
|
||||
modelName: 'open_uni_population_search',
|
||||
tableName: 'open_uni_population_searches',
|
||||
})
|
||||
export class OpenUniPopulationSearch extends Model<
|
||||
InferAttributes<OpenUniPopulationSearch>,
|
||||
InferCreationAttributes<OpenUniPopulationSearch>
|
||||
> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column(DataType.BIGINT)
|
||||
id!: CreationOptional<bigint>
|
||||
|
||||
@Column(DataType.BIGINT)
|
||||
userId!: bigint
|
||||
|
||||
@Column(DataType.STRING)
|
||||
name!: string
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
courseCodes!: string[]
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, DataType, Model, PrimaryKey, Table } from 'sequelize-typescript'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
timestamps: true,
|
||||
modelName: 'progress_criteria',
|
||||
tableName: 'progress_criteria',
|
||||
})
|
||||
export class ProgressCriteria extends Model<InferAttributes<ProgressCriteria>> {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
code!: string
|
||||
|
||||
@Column(DataType.STRING)
|
||||
curriculumVersion!: string
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
coursesYearOne!: string[]
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
coursesYearTwo!: string[]
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
coursesYearThree!: string[]
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
coursesYearFour!: string[]
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
coursesYearFive!: string[]
|
||||
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
coursesYearSix!: string[]
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
creditsYearOne!: number
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
creditsYearTwo!: number
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
creditsYearThree!: number
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
creditsYearFour!: number
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
creditsYearFive!: number
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
creditsYearSix!: number
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { AutoIncrement, Column, DataType, Model, PrimaryKey, Table, Unique } from 'sequelize-typescript'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
timestamps: true,
|
||||
modelName: 'study_guidance_group_tag',
|
||||
tableName: 'study_guidance_group_tags',
|
||||
})
|
||||
export class StudyGuidanceGroupTag extends Model<InferAttributes<StudyGuidanceGroupTag>> {
|
||||
@PrimaryKey
|
||||
@AutoIncrement
|
||||
@Column(DataType.BIGINT)
|
||||
id!: bigint
|
||||
|
||||
@Unique
|
||||
@Column(DataType.STRING)
|
||||
studyGuidanceGroupId!: string
|
||||
|
||||
@Column(DataType.STRING)
|
||||
studyProgramme!: string
|
||||
|
||||
@Column(DataType.STRING)
|
||||
year!: string
|
||||
}
|
||||