Compare commits
1134 Commits
main
...
stable/3.4
Author | SHA1 | Date |
---|---|---|
|
ba688a9668 | |
![]() |
5a8689827c | |
![]() |
2c1cc31e52 | |
![]() |
b2d05af928 | |
![]() |
27682f3742 | |
![]() |
58d5bf7d6c | |
![]() |
ff1d5c0c4d | |
![]() |
847b61f0ba | |
![]() |
420bef680a | |
![]() |
d716bd5414 | |
![]() |
941b0be524 | |
![]() |
a6446ce55c | |
![]() |
b24951d806 | |
![]() |
c035839892 | |
![]() |
fceb892005 | |
![]() |
31f59e069d | |
![]() |
23577be953 | |
![]() |
1e22ffbf7e | |
![]() |
b0bc4129ce | |
![]() |
87b8fcd42b | |
![]() |
93e0b53b9e | |
![]() |
2dfc926ba2 | |
![]() |
cbfffddec7 | |
![]() |
dc555a4116 | |
![]() |
d4953ed81a | |
![]() |
9cd0416938 | |
![]() |
19ea4a312e | |
![]() |
4efc591d21 | |
![]() |
cc6cf9ea42 | |
![]() |
740f164ac3 | |
![]() |
f04d64d48d | |
![]() |
e8803bde0c | |
![]() |
adb9d95ff3 | |
![]() |
6ac24f5f6c | |
![]() |
a3d8dd01da | |
![]() |
82ad1d2222 | |
![]() |
af4ff015ae | |
![]() |
34f27ea4c0 | |
![]() |
bbdbdd3c10 | |
![]() |
f2ae6d1cae | |
![]() |
420422e2cf | |
![]() |
8d20c40ca1 | |
![]() |
1b3af8cecd | |
![]() |
bd22957740 | |
![]() |
74b1527bbe | |
![]() |
91a9e19f09 | |
![]() |
932eb647ab | |
![]() |
0c30be5308 | |
![]() |
ac5a975a4c | |
![]() |
70d5b32bd7 | |
![]() |
eab60ab31a | |
![]() |
8c7a770f9b | |
![]() |
b2ae972ce0 | |
![]() |
8d90c6821e | |
![]() |
2287f3bf4f | |
![]() |
18ffcaca92 | |
![]() |
802ac1aee8 | |
![]() |
759e23e15b | |
![]() |
e8507414d9 | |
![]() |
c48658210b | |
![]() |
0ee14c9986 | |
![]() |
6c01c2b99b | |
![]() |
cd7421b732 | |
![]() |
1b2169dd45 | |
![]() |
b2ac759379 | |
![]() |
9d60951344 | |
![]() |
06a34a8f03 | |
![]() |
30bf5bc808 | |
![]() |
73f98ff55e | |
![]() |
d643028410 | |
![]() |
cbb9bd14d8 | |
![]() |
bf7eba603d | |
![]() |
7022ab8cec | |
![]() |
f86f8ec9b4 | |
![]() |
5bb9553cd3 | |
![]() |
d4df7ebaed | |
![]() |
b656d354cd | |
![]() |
98e4f05b90 | |
![]() |
301c9bd35a | |
![]() |
7dc35dce40 | |
![]() |
1d8856b501 | |
![]() |
157293530c | |
![]() |
ed64e7bd13 | |
![]() |
f00c4391a2 | |
![]() |
18f2e3326d | |
![]() |
5b28763bcc | |
![]() |
52f35c856d | |
![]() |
1c857fb08c | |
![]() |
0a3ea54a45 | |
![]() |
c593599e10 | |
![]() |
d17a050d8b | |
![]() |
19a9eedf17 | |
![]() |
27831154fa | |
![]() |
b4b0a958d5 | |
![]() |
5d13f94e40 | |
![]() |
0322436cf5 | |
![]() |
272c3de9fc | |
![]() |
416b2cc26f | |
![]() |
3532c55924 | |
![]() |
f1e2996c6e | |
![]() |
c0582e4b1c | |
![]() |
eed3bca25b | |
![]() |
57755ebf4b | |
![]() |
c286e0ff76 | |
![]() |
a14ab8c5e2 | |
![]() |
235bbf6c0d | |
![]() |
8423433f66 | |
![]() |
382fcf4a67 | |
![]() |
9a9ef8e039 | |
![]() |
eeb0e54985 | |
![]() |
5d434484b5 | |
![]() |
8d73f08072 | |
![]() |
d8713dad11 | |
![]() |
bf22e1b6e1 | |
![]() |
83949ad402 | |
![]() |
5a8a87070a | |
![]() |
bd3c8e09d8 | |
![]() |
d9d07c3c9b | |
![]() |
99acdab35f | |
![]() |
1daee9c6d6 | |
![]() |
a8611904cc | |
![]() |
6d63c90649 | |
![]() |
15fb19ad2f | |
![]() |
136e0a0569 | |
![]() |
1ed1896653 | |
![]() |
7a076b1d2d | |
![]() |
4938c34d50 | |
![]() |
8b22f5ff0e | |
![]() |
6b8ccb8c33 | |
![]() |
7dcdbb4ee1 | |
![]() |
51568b652e | |
![]() |
8c0aba83bc | |
![]() |
cccfec9154 | |
![]() |
1504e33607 | |
![]() |
d570ed2b72 | |
![]() |
5f8bb17cac | |
![]() |
654162dbce | |
![]() |
e0da18a0e6 | |
![]() |
946f07f34a | |
![]() |
3a2773aa5d | |
![]() |
e7448aa378 | |
![]() |
75ed60168e | |
![]() |
be36a8a40f | |
![]() |
37782fcae8 | |
![]() |
5d57301fb0 | |
![]() |
bf209d023c | |
![]() |
f3b6ceb9af | |
![]() |
e7dd5d87a2 | |
![]() |
899257791d | |
![]() |
3eb7386a58 | |
![]() |
c60553e376 | |
![]() |
8734e48aef | |
![]() |
79e5b8e337 | |
![]() |
6dc5c3bfa9 | |
![]() |
d4025296fe | |
![]() |
a7f392b997 | |
![]() |
6451cdf590 | |
![]() |
2eda8fea22 | |
![]() |
74b32af293 | |
![]() |
43d73a87f1 | |
![]() |
66945231f4 | |
![]() |
5a9767004d | |
![]() |
a0c9f3b6da | |
![]() |
f05dd8c448 | |
![]() |
14fe614c4d | |
![]() |
7a6b31d8f8 | |
![]() |
f707615875 | |
![]() |
fbb60cb0b6 | |
![]() |
861726fb9c | |
![]() |
4d4a3bb0db | |
![]() |
9a7c9c19a0 | |
![]() |
f4c237a18e | |
![]() |
710e825961 | |
![]() |
cb13c8cd47 | |
![]() |
83efb19562 | |
![]() |
964a41386d | |
![]() |
1e78f2a534 | |
![]() |
3402928935 | |
![]() |
2a7b55f11e | |
![]() |
18c75a6d12 | |
![]() |
7cf22b86ab | |
![]() |
0011d63a40 | |
![]() |
08fa38a89c | |
![]() |
5cee662058 | |
![]() |
0b23806db6 | |
![]() |
acf416b024 | |
![]() |
ec2c905f7d | |
![]() |
74981e26c0 | |
![]() |
261c69387f | |
![]() |
705c42bd11 | |
![]() |
d86fcd80b7 | |
![]() |
88e870c9df | |
![]() |
325469bc82 | |
![]() |
14687bba0e | |
![]() |
db4c593adf | |
![]() |
75bd3c50e5 | |
![]() |
d08752db21 | |
![]() |
85bb9f751d | |
![]() |
d030925e14 | |
![]() |
22df847c78 | |
![]() |
672e5ca544 | |
![]() |
9f543697ad | |
![]() |
c9c90cd4a3 | |
![]() |
91a15d24a8 | |
![]() |
8ad008d9de | |
![]() |
ee8702aff1 | |
![]() |
0d4673d182 | |
![]() |
f9d19db9e2 | |
![]() |
7315626e18 | |
![]() |
e2d4fafe6d | |
![]() |
fafcf5d583 | |
![]() |
0a2483a94b | |
![]() |
a8e329253c | |
![]() |
4f35770769 | |
![]() |
697177640b | |
![]() |
196e39ad15 | |
![]() |
ee31c1e633 | |
![]() |
bdbb5839cc | |
![]() |
dff5ae4a89 | |
![]() |
79d9781a1b | |
![]() |
e3c627b504 | |
![]() |
963986b91d | |
![]() |
2b3d1db3bf | |
![]() |
a1e7920b34 | |
![]() |
feb54c52a3 | |
![]() |
bb651b67eb | |
![]() |
5b58730cca | |
![]() |
23fcc3a7d0 | |
![]() |
d80919f0e7 | |
![]() |
8891a52bdc | |
![]() |
26d5d81b6f | |
![]() |
1e6b042d71 | |
![]() |
f54050a83c | |
![]() |
a94f00672b | |
![]() |
7ca761bdb0 | |
![]() |
1edf4437a0 | |
![]() |
ba177a89d4 | |
![]() |
d059afac5a | |
![]() |
045648eddb | |
![]() |
787390c899 | |
![]() |
4b13ff681e | |
![]() |
6430727590 | |
![]() |
83549ce02b | |
![]() |
dca6143041 | |
![]() |
2d4419530e | |
![]() |
28980afbd5 | |
![]() |
7275aa69af | |
![]() |
1ad0a61524 | |
![]() |
ca298960ae | |
![]() |
2f9b6aba95 | |
![]() |
49dba31d56 | |
![]() |
f68fca8c83 | |
![]() |
2e43a17987 | |
![]() |
2e5cea512e | |
![]() |
9c2e49692c | |
![]() |
c9b924d79a | |
![]() |
644d3b2fee | |
![]() |
f897bb01a3 | |
![]() |
41a3447357 | |
![]() |
7bbd8688a2 | |
![]() |
c10a0ad70d | |
![]() |
3595245663 | |
![]() |
bd29777d83 | |
![]() |
eadb0e2f0e | |
![]() |
d1aba2ef94 | |
![]() |
2e1690d2d0 | |
![]() |
a48fe86791 | |
![]() |
63132fdbc5 | |
![]() |
e45d83de5a | |
![]() |
365d0d55ea | |
![]() |
52108cd0c4 | |
![]() |
520915c3f5 | |
![]() |
b76e75ae96 | |
![]() |
1a538e241d | |
![]() |
a9d223121e | |
![]() |
4d706f648f | |
![]() |
ba0e9b3bc6 | |
![]() |
92afd07b62 | |
![]() |
3ba4f99876 | |
![]() |
bef322d0a4 | |
![]() |
a11682a708 | |
![]() |
bc9b028624 | |
![]() |
291402e159 | |
![]() |
bd7fa9b3db | |
![]() |
ac50d6f8dc | |
![]() |
265145f001 | |
![]() |
39fb2fadec | |
![]() |
d62d1a687b | |
![]() |
d0c1879521 | |
![]() |
93da435e7c | |
![]() |
45948c47fb | |
![]() |
3504a87295 | |
![]() |
d83b7c0ea9 | |
![]() |
155ade1a8c | |
![]() |
f80ac1d9c5 | |
![]() |
2c7196493d | |
![]() |
46223328f7 | |
![]() |
619a698272 | |
![]() |
a450a1dff5 | |
![]() |
d73b2142b7 | |
![]() |
05fcf40b51 | |
![]() |
83ff7b938c | |
![]() |
7314bf0999 | |
![]() |
d3530a3657 | |
![]() |
3d8f3a69af | |
![]() |
7dcc0ad42a | |
![]() |
c35cacebb1 | |
![]() |
614e0f2d5f | |
![]() |
4e6f0850c4 | |
![]() |
33fca309c4 | |
![]() |
b13732f9ec | |
![]() |
0a8274e6e2 | |
![]() |
a9045b6a1c | |
![]() |
2d4ece84a0 | |
![]() |
a796b2a8b8 | |
![]() |
cd98502b1c | |
![]() |
8d393ba17f | |
![]() |
e75bdeb0f7 | |
![]() |
f8446ee609 | |
![]() |
baad950698 | |
![]() |
4929e2b6f6 | |
![]() |
e72cebca4a | |
![]() |
e11f0f6f25 | |
![]() |
9f16bfee21 | |
![]() |
5152a448be | |
![]() |
500b773ee1 | |
![]() |
a8fcb89f48 | |
![]() |
085ada3dc4 | |
![]() |
ce3e2588c5 | |
![]() |
7e875c45db | |
![]() |
e74df38a0f | |
![]() |
ef451afae1 | |
![]() |
4490ee91d0 | |
![]() |
3012fabf4f | |
![]() |
fe0f8d28f4 | |
![]() |
a3a126f930 | |
![]() |
69bf46a5ff | |
![]() |
5b91ba4597 | |
![]() |
35e8f84fda | |
![]() |
05ec54927b | |
![]() |
e9c2dc90d5 | |
![]() |
7af39a5570 | |
![]() |
eda242e83f | |
![]() |
c1b4d3154d | |
![]() |
1cf2763ed6 | |
![]() |
c0dad72eb4 | |
![]() |
aac8ec8f2e | |
![]() |
69adc1d41c | |
![]() |
b0ccb1ea7e | |
![]() |
7fffffb497 | |
![]() |
f65e8ae819 | |
![]() |
8e5b1fa99d | |
![]() |
8e98966db2 | |
![]() |
0ed4e27725 | |
![]() |
5910d2c914 | |
![]() |
612bf78871 | |
![]() |
c569835ce1 | |
![]() |
a1e65e8a47 | |
![]() |
67202a4a4b | |
![]() |
ac0a27276c | |
![]() |
ab2a8ca419 | |
![]() |
180de2d3a9 | |
![]() |
d7d8dcb3c9 | |
![]() |
d05958ca10 | |
![]() |
f7d228a600 | |
![]() |
40dc0e08fa | |
![]() |
470168c58c | |
![]() |
b08948f3e5 | |
![]() |
c3de6dc870 | |
![]() |
09bc7f093a | |
![]() |
372f1eaa7e | |
![]() |
f57fe05e26 | |
![]() |
26744fde9f | |
![]() |
1b482871ac | |
![]() |
8746496d2d | |
![]() |
cc5bcf1a81 | |
![]() |
f74cf10ff3 | |
![]() |
9e38ed955f | |
![]() |
058a7f71ae | |
![]() |
799dd08e0d | |
![]() |
3b1b396e9a | |
![]() |
72b7162eeb | |
![]() |
dc58752575 | |
![]() |
5b2f8409e4 | |
![]() |
a839294add | |
![]() |
e5f0ebd6e5 | |
![]() |
bd65b5d41c | |
![]() |
94c943cdb5 | |
![]() |
04eedc7c37 | |
![]() |
a20641fe44 | |
![]() |
024b9c74e6 | |
![]() |
1380812924 | |
![]() |
cc65b756c7 | |
![]() |
e3d718cad0 | |
![]() |
47a2204921 | |
![]() |
878f3a7ab3 | |
![]() |
676bda8cc3 | |
![]() |
2130f4970f | |
![]() |
11d7f7b888 | |
![]() |
7e67e0db12 | |
![]() |
97950d5baa | |
![]() |
8049bfa91e | |
![]() |
0f0d750d83 | |
![]() |
4f4bff9bb3 | |
![]() |
b2e6d2f2ac | |
![]() |
b200cfbd07 | |
![]() |
e225a57f97 | |
![]() |
1145ae1460 | |
![]() |
bc382df68f | |
![]() |
ea180246c7 | |
![]() |
0b01b5576b | |
![]() |
7e763e8c07 | |
![]() |
010b61cce2 | |
![]() |
8542817129 | |
![]() |
b0ba30b454 | |
![]() |
183c511046 | |
![]() |
ea277adf9e | |
![]() |
ef135837f7 | |
![]() |
a37e2196b3 | |
![]() |
273debf99a | |
![]() |
ab2fbaac79 | |
![]() |
aeaedabb87 | |
![]() |
52c4aa6c58 | |
![]() |
227dfd0c26 | |
![]() |
b179930cc8 | |
![]() |
3b062a52e7 | |
![]() |
266c129e04 | |
![]() |
f07cc4e176 | |
![]() |
222a2ea581 | |
![]() |
057e03a82c | |
![]() |
1ebad842de | |
![]() |
cb73f52345 | |
![]() |
53be648c23 | |
![]() |
2a224cb3b5 | |
![]() |
acd33b8207 | |
![]() |
e2cabbaf62 | |
![]() |
19df8184d0 | |
![]() |
0a80a73f2e | |
![]() |
b9f36f1cea | |
![]() |
5d5a5b3e39 | |
![]() |
ef42a2293d | |
![]() |
ac63a04666 | |
![]() |
78cfa4875e | |
![]() |
689bfcac61 | |
![]() |
ec36d4d64e | |
![]() |
5cc464b250 | |
![]() |
92cf811921 | |
![]() |
1af78df328 | |
![]() |
9c738b5d8e | |
![]() |
9ddb3a9179 | |
![]() |
10614ca57b | |
![]() |
182759e794 | |
![]() |
78f0b29921 | |
![]() |
2943c969ab | |
![]() |
ea4ec53fb1 | |
![]() |
944685696a | |
![]() |
5629c73b4b | |
![]() |
d0ed5448e8 | |
![]() |
00b148edbd | |
![]() |
5e1b5b5658 | |
![]() |
96d464bcfa | |
![]() |
85de17611f | |
![]() |
a410083349 | |
![]() |
bdd44f78eb | |
![]() |
ce2f71a9da | |
![]() |
90ac27ff43 | |
![]() |
652b727386 | |
![]() |
e1183fff60 | |
![]() |
629ad4ec1f | |
![]() |
00066806d6 | |
![]() |
96f96f09ee | |
![]() |
48c09ed4c5 | |
![]() |
c4f37999f3 | |
![]() |
ad907de958 | |
![]() |
8b94829a2c | |
![]() |
fbed661dfb | |
![]() |
b318bfda99 | |
![]() |
5577bac7c9 | |
![]() |
c7405c36d8 | |
![]() |
fca688a1f7 | |
![]() |
26ac618ddf | |
![]() |
c323eabd6f | |
![]() |
a51103b7b7 | |
![]() |
d6467d5bbf | |
![]() |
00bb266098 | |
![]() |
0a67c24138 | |
![]() |
667a841051 | |
![]() |
fd7a4cb64b | |
![]() |
4f24a38da8 | |
![]() |
03cb8592fe | |
![]() |
1f302b466a | |
![]() |
65c7d3491c | |
![]() |
b611642ecb | |
![]() |
1eee3bc56d | |
![]() |
b5cb694fc7 | |
![]() |
f609e6362f | |
![]() |
36506a7383 | |
![]() |
07a003717d | |
![]() |
ab230fe7a9 | |
![]() |
26e414e3d1 | |
![]() |
f3809fc8a9 | |
![]() |
bed9b3a958 | |
![]() |
5b2fe01965 | |
![]() |
38534d4e01 | |
![]() |
6c35e225a5 | |
![]() |
6abaeb2155 | |
![]() |
d446382f70 | |
![]() |
34070843c2 | |
![]() |
01206cb7c6 | |
![]() |
2736917c7e | |
![]() |
72dc55558f | |
![]() |
9ce8fe8233 | |
![]() |
dd3dbea482 | |
![]() |
a188abed48 | |
![]() |
ade2d4b977 | |
![]() |
be0deefdce | |
![]() |
7b0f8e3c25 | |
![]() |
88b25acd0a | |
![]() |
22f9108b49 | |
![]() |
019c097c26 | |
![]() |
f9796027ef | |
![]() |
2364ed66ff | |
![]() |
9fcb6cdcba | |
![]() |
5b84bddc2a | |
![]() |
def6e8d59d | |
![]() |
a49ed17b45 | |
![]() |
7d97cede2d | |
![]() |
17e5d42d17 | |
![]() |
4c7bf0a203 | |
![]() |
85852d158a | |
![]() |
beb59cee73 | |
![]() |
d89c7cfdb0 | |
![]() |
883463ea87 | |
![]() |
0c66afc34a | |
![]() |
513f1477af | |
![]() |
837af97d57 | |
![]() |
6bc2c104b1 | |
![]() |
d4b92a2b4e | |
![]() |
7ad3b78eb2 | |
![]() |
5ef1869a10 | |
![]() |
ccc48e6b3f | |
![]() |
866acfe7f5 | |
![]() |
6943c3d18f | |
![]() |
b391ed0dfe | |
![]() |
06044e81c0 | |
![]() |
eacccd8f5c | |
![]() |
61c5f77d29 | |
![]() |
5502e5337a | |
![]() |
0956153ea4 | |
![]() |
266f9b73e9 | |
![]() |
d4577ed8aa | |
![]() |
582215042d | |
![]() |
1dd86a29be | |
![]() |
961a2da888 | |
![]() |
2b5abf72a4 | |
![]() |
7277a1bb01 | |
![]() |
b214a69136 | |
![]() |
b864d67cda | |
![]() |
2305ca9d21 | |
![]() |
ca56b4f8b4 | |
![]() |
d2043f508c | |
![]() |
d317e032e7 | |
![]() |
6c60834f37 | |
![]() |
4fef8ed4dc | |
![]() |
b9f78f501d | |
![]() |
f809db0430 | |
![]() |
1707c1f4fd | |
![]() |
12e6090fa7 | |
![]() |
d739d401c4 | |
![]() |
b050a87bb2 | |
![]() |
e77b2518d5 | |
![]() |
21990aa568 | |
![]() |
58db337a40 | |
![]() |
70ea4f3658 | |
![]() |
25d83b4419 | |
![]() |
22a318bde2 | |
![]() |
fd2fd8d73a | |
![]() |
823a87c164 | |
![]() |
2162f2b049 | |
![]() |
28be46cf5a | |
![]() |
435bb59472 | |
![]() |
de474e9eae | |
![]() |
706c1d9e36 | |
![]() |
e6fc32b9b4 | |
![]() |
2bb0134cd8 | |
![]() |
be3fafd907 | |
![]() |
daaf404756 | |
![]() |
d22e0bf2f6 | |
![]() |
ed1c3eaa7a | |
![]() |
9607f05454 | |
![]() |
2ca157bb7c | |
![]() |
25878f297f | |
![]() |
8d2a7f1b12 | |
![]() |
a94ce67c22 | |
![]() |
deddd68121 | |
![]() |
28bac117be | |
![]() |
98a8de3c2d | |
![]() |
49a3bcd930 | |
![]() |
8c28b03ffc | |
![]() |
3ac8569712 | |
![]() |
c0fb65316c | |
![]() |
90e13a0f8e | |
![]() |
88994efac3 | |
![]() |
ed2c298928 | |
![]() |
677a93e2ca | |
![]() |
991c08d57d | |
![]() |
adcf98a69b | |
![]() |
6606e46f68 | |
![]() |
e1d4a4152a | |
![]() |
42dd397fae | |
![]() |
3b2fbe8915 | |
![]() |
69299808b6 | |
![]() |
cf4573cb54 | |
![]() |
b392ac83aa | |
![]() |
7f53636b7b | |
![]() |
40c2a7fae4 | |
![]() |
bc540180dd | |
![]() |
ec13ab56e8 | |
![]() |
acbddd3c53 | |
![]() |
6007799f1d | |
![]() |
2687d1abba | |
![]() |
c4a2b02f5d | |
![]() |
e8e39b1e89 | |
![]() |
a42205e47f | |
![]() |
855db8241b | |
![]() |
688b1b276d | |
![]() |
3d7bfe652c | |
![]() |
1a0e017f80 | |
![]() |
df2e26c3ed | |
![]() |
4712707d6b | |
![]() |
909a7539c5 | |
![]() |
51512fd589 | |
![]() |
34f23b3d0e | |
![]() |
8d92353047 | |
![]() |
04477d9ebd | |
![]() |
bc333a6b51 | |
![]() |
594777960b | |
![]() |
2759f8ce2b | |
![]() |
b555de8510 | |
![]() |
b596bf0ca5 | |
![]() |
d893f3dbe5 | |
![]() |
9148d97f7a | |
![]() |
ed9e50a1b4 | |
![]() |
eb98289b84 | |
![]() |
f3fe98436e | |
![]() |
5b63809b12 | |
![]() |
a408ee62ee | |
![]() |
7446effe0f | |
![]() |
fb27f8ce8a | |
![]() |
792f0e5d06 | |
![]() |
392c32fd92 | |
![]() |
28878a0b12 | |
![]() |
059ace3a11 | |
![]() |
06974b559e | |
![]() |
7a1e7c298d | |
![]() |
ef87f05454 | |
![]() |
a329031942 | |
![]() |
0367398cb5 | |
![]() |
325c5ea1f4 | |
![]() |
9a4f8e1781 | |
![]() |
38af3d3b8a | |
![]() |
4960a8f115 | |
![]() |
0d16b487d5 | |
![]() |
756fdc9c66 | |
![]() |
fcdfad1c2e | |
![]() |
5070069910 | |
![]() |
a9c1578ebb | |
![]() |
aed17360e6 | |
![]() |
d8f62a05ba | |
![]() |
1b6b70c080 | |
![]() |
7bf8e880fd | |
![]() |
7af65f790e | |
![]() |
0933bb6abd | |
![]() |
216e4f00a3 | |
![]() |
8faa2ad38f | |
![]() |
9ddf9ddb8c | |
![]() |
251296f42f | |
![]() |
9a2d3a3760 | |
![]() |
3cb3ef2974 | |
![]() |
2b7e4d3d19 | |
![]() |
d1640bc98d | |
![]() |
1c0724341c | |
![]() |
418480bff5 | |
![]() |
9c9f268fbf | |
![]() |
f694e9b2c4 | |
![]() |
774fa4c204 | |
![]() |
13db5687cb | |
![]() |
63c4bc3ff7 | |
![]() |
4f194a8794 | |
![]() |
d48794ae8a | |
![]() |
683aed56bb | |
![]() |
030378b48a | |
![]() |
2bcab5d098 | |
![]() |
2c85bb28f1 | |
![]() |
2b55388870 | |
![]() |
fbf424e570 | |
![]() |
2e8e32454e | |
![]() |
389a244615 | |
![]() |
a46d8ec7ad | |
![]() |
b59c69e086 | |
![]() |
2b3766b758 | |
![]() |
c0f5c7b548 | |
![]() |
c6abbb629e | |
![]() |
a40657e153 | |
![]() |
e75573e139 | |
![]() |
5f5f704057 | |
![]() |
b726801747 | |
![]() |
a48592af50 | |
![]() |
72a53c5cd0 | |
![]() |
d682d0d134 | |
![]() |
bfe72497cd | |
![]() |
c6bc5978e2 | |
![]() |
65ee468c21 | |
![]() |
09a10c7e92 | |
![]() |
ccc3e38427 | |
![]() |
8d25f6ae15 | |
![]() |
23ae32a758 | |
![]() |
fbbcd6fa94 | |
![]() |
2c17d7b7aa | |
![]() |
e268903536 | |
![]() |
6e2e1ebe7a | |
![]() |
30e8f7d87f | |
![]() |
9aefb122e6 | |
![]() |
5618c04416 | |
![]() |
ee344032b7 | |
![]() |
6e80ff5f00 | |
![]() |
47113f14fc | |
![]() |
7d912d82de | |
![]() |
ebf8325ded | |
![]() |
03acae26ff | |
![]() |
271ccdd46a | |
![]() |
109fea791d | |
![]() |
c2bd7c16a9 | |
![]() |
7ded2cd8a1 | |
![]() |
85a22ed99c | |
![]() |
e2597002e2 | |
![]() |
01ce1409d3 | |
![]() |
74e3ea119e | |
![]() |
9e55cb1480 | |
![]() |
20175a1a6b | |
![]() |
719d1d1cf1 | |
![]() |
2835e746e8 | |
![]() |
a7703a5557 | |
![]() |
da4092768e | |
![]() |
32775b0a2a | |
![]() |
011c23093f | |
![]() |
3063a9e9fc | |
![]() |
656fcccee1 | |
![]() |
e35b658731 | |
![]() |
d76d74e225 | |
![]() |
9eeb287425 | |
![]() |
5666749e62 | |
![]() |
eeb97c44fd | |
![]() |
fa1347f611 | |
![]() |
278b33c2d7 | |
![]() |
1cb8ef2d14 | |
![]() |
ba3c5e07f7 | |
![]() |
55f1d02fcc | |
![]() |
378d091dbd | |
![]() |
cb8f219163 | |
![]() |
66757b04ae | |
![]() |
346413fbb0 | |
![]() |
cb190331f3 | |
![]() |
23ee6a2951 | |
![]() |
f59ce9ef3b | |
![]() |
f5654f3a8c | |
![]() |
4a96aa31c1 | |
![]() |
fab51091b1 | |
![]() |
c1d63b320d | |
![]() |
988ee0fe93 | |
![]() |
3d252060c9 | |
![]() |
6898458695 | |
![]() |
c2a1b62c8b | |
![]() |
bb10c25974 | |
![]() |
fde745530e | |
![]() |
9a47cff7fa | |
![]() |
22a374a150 | |
![]() |
f70953f454 | |
![]() |
435f555559 | |
![]() |
9cf602f0c1 | |
![]() |
2fd4e70b0c | |
![]() |
81b021ab47 | |
![]() |
fd371b87e4 | |
![]() |
e20c93d445 | |
![]() |
55f65576f0 | |
![]() |
d558c293b2 | |
![]() |
44f1d1e819 | |
![]() |
677595fe5b | |
![]() |
912a528f8a | |
![]() |
9feaa59ebb | |
![]() |
b712af2d6d | |
![]() |
81c2df3458 | |
![]() |
6a59e678a9 | |
![]() |
00e644292d | |
![]() |
b43151fd59 | |
![]() |
fbbc4389fb | |
![]() |
d53e85b853 | |
![]() |
68c77fe52c | |
![]() |
bc1373b696 | |
![]() |
b9fbf4209b | |
![]() |
ec2ec08333 | |
![]() |
958f0fb786 | |
![]() |
ac4cb39105 | |
![]() |
b5bc855dfe | |
![]() |
1f876ec6dd | |
![]() |
c1605929e9 | |
![]() |
2ea95937d7 | |
![]() |
a80915397d | |
![]() |
f06f2dee9f | |
![]() |
33ba8c4628 | |
![]() |
dc7dfc1936 | |
![]() |
7d3280707d | |
![]() |
13cbece9d9 | |
![]() |
5ed9c88ae4 | |
![]() |
5239e40858 | |
![]() |
081f13e2ff | |
![]() |
438b3558bf | |
![]() |
ff4324117e | |
![]() |
f590994875 | |
![]() |
2cdb3f4ef3 | |
![]() |
e3c1d5432b | |
![]() |
9387a3f394 | |
![]() |
1853028cf0 | |
![]() |
56b47214bc | |
![]() |
43b13e314e | |
![]() |
0d9738b72d | |
![]() |
47795b57d1 | |
![]() |
7d455b34f5 | |
![]() |
fbb0be6fb4 | |
![]() |
acf499f6e1 | |
![]() |
79e3780a26 | |
![]() |
e653021eff | |
![]() |
aeb893a8d9 | |
![]() |
82efbe76bd | |
![]() |
ff9125fb9f | |
![]() |
d4f211e344 | |
![]() |
4673c741e9 | |
![]() |
e1345cb808 | |
![]() |
bf35c55956 | |
![]() |
6efdc9a3dd | |
![]() |
cadef6d42e | |
![]() |
bc3b8be78d | |
![]() |
18bc495bd8 | |
![]() |
8451cd2d88 | |
![]() |
5072e66a7e | |
![]() |
3109337004 | |
![]() |
3ca4714812 | |
![]() |
429473dcf9 | |
![]() |
c186a575f6 | |
![]() |
c4f482b70c | |
![]() |
0275df6ab2 | |
![]() |
dced8fbcc7 | |
![]() |
f4907e6604 | |
![]() |
d7408b40f9 | |
![]() |
e215a23b80 | |
![]() |
a31fa7dda6 | |
![]() |
7665634d42 | |
![]() |
9c7b9b0920 | |
![]() |
0eee839736 | |
![]() |
a84bfccd07 | |
![]() |
600b9c148b | |
![]() |
d8b21c5fb5 | |
![]() |
dcf5d5316c | |
![]() |
fba043fedf | |
![]() |
762d1f9912 | |
![]() |
60621bf4d0 | |
![]() |
bf88cea200 | |
![]() |
23842fd496 | |
![]() |
4ac7b1eb4b | |
![]() |
17049cc0f3 | |
![]() |
fd026e165f | |
![]() |
e52697ad7e | |
![]() |
0c93c44f0d | |
![]() |
4b95398ac1 | |
![]() |
37c3ac5aff | |
![]() |
3f03f27cdb | |
![]() |
f694e2355d | |
![]() |
3820e09b89 | |
![]() |
1ca3196a75 | |
![]() |
ee6076f168 | |
![]() |
b6bb1fe767 | |
![]() |
7609a0c3db | |
![]() |
b090e46b66 | |
![]() |
ca039860f7 | |
![]() |
fca4154bb5 | |
![]() |
621d0f4e1a | |
![]() |
d1b6ed8d29 | |
![]() |
8058a4d695 | |
![]() |
853bc31e21 | |
![]() |
fa63ef0307 | |
![]() |
fef3cf41bb | |
![]() |
34d85c996c | |
![]() |
b7b27d2e88 | |
![]() |
b0bf4990f8 | |
![]() |
0ee70b7434 | |
![]() |
9938a68865 | |
![]() |
3e19840b08 | |
![]() |
7a31cff612 | |
![]() |
e7de593b54 | |
![]() |
602d1c8e7b | |
![]() |
c5dd2ea261 | |
![]() |
8796eeeb62 | |
![]() |
25839ea709 | |
![]() |
ea830f53b0 | |
![]() |
c643a233ae | |
![]() |
5aa895bda2 | |
![]() |
2910701422 | |
![]() |
1e2395c1e6 | |
![]() |
fede11b59f | |
![]() |
77cf3e2785 | |
![]() |
4e624384e7 | |
![]() |
f9cd3ebd89 | |
![]() |
6a6e90067a | |
![]() |
1a653c3fa7 | |
![]() |
b51787129b | |
![]() |
e0069f734a | |
![]() |
f415fd0554 | |
![]() |
c6836ff6c5 | |
![]() |
4a24da12da | |
![]() |
3842f66877 | |
![]() |
38ee6bb2f1 | |
![]() |
a47285c0ff | |
![]() |
1439444b2e | |
![]() |
cce76118c3 | |
![]() |
aa1a2cec89 | |
![]() |
46d0bbd8f5 | |
![]() |
b78372f8a3 | |
![]() |
fd9b8b1c5c | |
![]() |
7a25a2496d | |
![]() |
ddfe7d0c5a | |
![]() |
152401a9a3 | |
![]() |
2057150076 | |
![]() |
cb52347354 | |
![]() |
3169e4f30b | |
![]() |
4221351223 | |
![]() |
0c6da9799c | |
![]() |
a71e36c861 | |
![]() |
41b9065807 | |
![]() |
527f947143 | |
![]() |
c8faa982ac | |
![]() |
38486463bc | |
![]() |
6a488eb78e | |
![]() |
0aef3f79ce | |
![]() |
97c2299aec | |
![]() |
e702843f07 | |
![]() |
0f3d07f151 | |
![]() |
aa097ee689 | |
![]() |
f7a97cf886 | |
![]() |
25f8f42c92 | |
![]() |
523eb96f9d | |
![]() |
2c548d2dfb | |
![]() |
91d4b3c7af | |
![]() |
d210496146 | |
![]() |
35ce596706 | |
![]() |
f007e07544 | |
![]() |
70aadcdd28 | |
![]() |
9ffbb39e95 | |
![]() |
170aa1c8f0 | |
![]() |
ad4ed3443a | |
![]() |
42fbe93314 | |
![]() |
6cdf9a5582 | |
![]() |
75ebf5bc77 | |
![]() |
c26ef8c0bb | |
![]() |
6eae497abe | |
![]() |
1570b5b806 | |
![]() |
537eeadce4 | |
![]() |
96ee1c0af3 | |
![]() |
99416e3043 | |
![]() |
0f8167e39c | |
![]() |
9864ff3847 | |
![]() |
a7518ed5b6 | |
![]() |
5b7bbfd0bb | |
![]() |
b7566fcc69 | |
![]() |
82c6929a8d | |
![]() |
35a67017a3 | |
![]() |
4841343c02 | |
![]() |
7a97aa1b79 | |
![]() |
12bc926b44 | |
![]() |
53b4b1c1f9 | |
![]() |
cc372cfba5 | |
![]() |
b7b8620153 | |
![]() |
7882ea1a25 | |
![]() |
04a7ce22fd | |
![]() |
820a47123a | |
![]() |
42af962248 | |
![]() |
7b5f2648af | |
![]() |
a1e2c49815 | |
![]() |
e1acf6e9d6 | |
![]() |
83d57e9da7 | |
![]() |
bb2f958eb5 | |
![]() |
7b0a2d8ec2 | |
![]() |
b2d05f81fe | |
![]() |
4419e76223 | |
![]() |
1e3c83babc | |
![]() |
3be28ec50a | |
![]() |
baa1787189 | |
![]() |
8119507b8a | |
![]() |
39ccfe3147 | |
![]() |
106816a733 | |
![]() |
c257baa14b | |
![]() |
04c625b3d5 | |
![]() |
d646691961 | |
![]() |
aaea4ec2e9 | |
![]() |
5b878f4814 | |
![]() |
5bdbe4778a | |
![]() |
fbff4de431 | |
![]() |
af6c5faac8 | |
![]() |
14de67a09d | |
![]() |
6f7c6036c2 | |
![]() |
19af02a315 | |
![]() |
d50899c407 | |
![]() |
73fc936306 | |
![]() |
c2406fcc03 | |
![]() |
557824f5f1 | |
![]() |
91be76a263 | |
![]() |
eadc09dc56 | |
![]() |
c43e180494 | |
![]() |
6fddddd9f4 | |
![]() |
cf50295ca4 | |
![]() |
7af2f70494 | |
![]() |
cd3435064c | |
![]() |
123df7660f | |
![]() |
2fb372ead9 | |
![]() |
7d86f62e2d | |
![]() |
d92622410f | |
![]() |
99c3afb417 | |
![]() |
23a105bdb8 | |
![]() |
bf0eadebb7 | |
![]() |
fe71322199 | |
![]() |
5bf3dfadff | |
![]() |
5617b02804 | |
![]() |
5a6d2d2e42 | |
![]() |
661fd55c67 | |
![]() |
072ec937a1 | |
![]() |
b873dc156b | |
![]() |
4acadd33ca | |
![]() |
f0e396b3a4 | |
![]() |
73eff81edd | |
![]() |
54dd97399e | |
![]() |
ee07e8f0ce | |
![]() |
d12e052030 | |
![]() |
0ab4532ac8 | |
![]() |
58483d7024 | |
![]() |
3c9f6ed278 | |
![]() |
64f2720b1a | |
![]() |
d15c9892ed | |
![]() |
ee4c6aa0bf | |
![]() |
a05662a0f8 | |
![]() |
29a9a09bc6 | |
![]() |
3c36441967 | |
![]() |
8fe5a0c9f4 | |
![]() |
61b7731073 | |
![]() |
e2feeb4b65 | |
![]() |
53b9ce73f2 | |
![]() |
9d7028ea5f | |
![]() |
72678770bb | |
![]() |
82c8ade0ba | |
![]() |
2d13519c35 | |
![]() |
e72bcc1eaf | |
![]() |
97a5bb4aa6 | |
![]() |
7598fc5367 | |
![]() |
b48ca8c434 | |
![]() |
6ba0d0c5e6 | |
![]() |
0b37c5a857 | |
![]() |
d4599a435b | |
![]() |
93dc78c7d6 | |
![]() |
6044c63c28 | |
![]() |
524a97cdcc | |
![]() |
6c1317e25f | |
![]() |
294b75c320 | |
![]() |
09b0d19de0 | |
![]() |
df1047fc76 | |
![]() |
bc54a6eb46 | |
![]() |
1de73d5701 | |
![]() |
a0c3a28456 | |
![]() |
c46369c6a7 | |
![]() |
b16afaa285 | |
![]() |
e2585fb757 | |
![]() |
84a39ccb62 | |
![]() |
682db96b7c | |
![]() |
604df9d48b | |
![]() |
7ab5346198 | |
![]() |
e67ca77ad1 | |
![]() |
fff1f15b6c | |
![]() |
96aa3b0084 | |
![]() |
72ff1b1f09 | |
![]() |
fafb81daca | |
![]() |
b50cf42543 | |
![]() |
90b04366b5 | |
![]() |
8d77c0495b | |
![]() |
1b761d31c0 | |
![]() |
09ef3c5071 | |
![]() |
046a152ec5 | |
![]() |
6605934a33 | |
![]() |
1246dd54ad | |
![]() |
5fa8341614 | |
![]() |
ce171980e8 | |
![]() |
ced40cab74 | |
![]() |
4d4697eee0 | |
![]() |
aa46922c8b | |
![]() |
ec17376e8e | |
![]() |
35d9fd9d8e | |
![]() |
7acf2157fa | |
![]() |
70fc5a69ab | |
![]() |
3ad8944b9c | |
![]() |
847482bb5f | |
![]() |
219103129d | |
![]() |
13de88c136 | |
![]() |
98146a29c7 | |
![]() |
758e059f9b | |
![]() |
7204d59d66 | |
![]() |
76bd184ff4 | |
![]() |
fbe5ea2056 | |
![]() |
2236f63fe9 | |
![]() |
ec79f70648 | |
![]() |
0267b0cb42 | |
![]() |
2ac01a5ea3 | |
![]() |
a51720e18b | |
![]() |
27e8301131 | |
![]() |
407a430419 | |
![]() |
a6bdaedff1 | |
![]() |
59795f32e3 | |
![]() |
a161bca028 | |
![]() |
6f114d0072 | |
![]() |
8012bfbfc0 | |
![]() |
d311042806 | |
![]() |
faf8004280 | |
![]() |
c2ad39a2c5 | |
![]() |
7a23139f5e | |
![]() |
b9e40717de | |
![]() |
5f8e64140a | |
![]() |
a2d561f667 | |
![]() |
b3c98dd207 | |
![]() |
a35fa105ed |
|
@ -10,11 +10,12 @@ assignees: ""
|
|||
A clear and concise description of what the bug is.
|
||||
|
||||
**How to Reproduce**
|
||||
Steps to reproduce the behavior: preferably on [nighty](https://nightly.demo.openslides.org)
|
||||
Steps to reproduce the behavior: preferably on [nightly](https://nightly.demo.openslides.org) (for
|
||||
3.4) or the [OS4 demo](https://demo.os4.openslides.com/).
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
2. Click on '...'
|
||||
3. Scroll down to '...'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
|
@ -22,16 +23,16 @@ A clear and concise description of what you expected to happen.
|
|||
|
||||
**System information**
|
||||
|
||||
- OpenSlides-Version: [e.g. 2.1, 2.2, 2.3, 3,0]
|
||||
- Additional version information (if any): [e.g. commit hash or installation-month]
|
||||
- Python Version: [e.g. 3,5, 3.6, 3.7]
|
||||
- Device: [e.g. iPhone6, Notebook]
|
||||
- OS: [e.g. Andoird 7, iOS8.1, Windows 10]
|
||||
- OpenSlides-Version: [e.g. 3.4, 4.0-beta]
|
||||
- Additional version information (if any): [e.g. commit hash or installation month]
|
||||
- Python Version: [e.g. 3.6, 3.7, 3.8]
|
||||
- Device: [e.g. iPhone 6, Notebook]
|
||||
- OS: [e.g. Android 7, iOS 8.1, Windows 10]
|
||||
- Browser: [e.g. chrome, firefox, opera, edge, safari]
|
||||
- Browser-Version: [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other information to comprehend your problem
|
||||
Add any other information to comprehend your problem.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
version: "3"
|
||||
services:
|
||||
backend:
|
||||
image: openslides-backend
|
||||
ports:
|
||||
- "9002:9002"
|
||||
environment:
|
||||
- DATASTORE_READER_HOST=reader
|
||||
- DATASTORE_READER_PORT=9010
|
||||
- DATASTORE_WRITER_HOST=writer
|
||||
- DATASTORE_WRITER_PORT=9011
|
||||
depends_on:
|
||||
- writer
|
||||
- reader
|
||||
reader:
|
||||
image: openslides-datastore-reader
|
||||
environment:
|
||||
- DATASTORE_ENABLE_DEV_ENVIRONMENT=1
|
||||
depends_on:
|
||||
- postgresql
|
||||
ports:
|
||||
- "9010:9010"
|
||||
writer:
|
||||
image: openslides-datastore-writer
|
||||
environment:
|
||||
- DATASTORE_ENABLE_DEV_ENVIRONMENT=1
|
||||
- MESSAGE_BUS_HOST=redis
|
||||
- MESSAGE_BUS_PORT=6379
|
||||
depends_on:
|
||||
- postgresql
|
||||
- redis
|
||||
postgresql:
|
||||
image: postgres:11
|
||||
environment:
|
||||
- POSTGRES_USER=openslides
|
||||
- POSTGRES_PASSWORD=openslides
|
||||
- POSTGRES_DB=openslides
|
||||
redis:
|
||||
image: redis:alpine
|
|
@ -1,90 +0,0 @@
|
|||
---
|
||||
name: Build Docker images for all OpenSlides services
|
||||
on: [push, workflow_dispatch]
|
||||
|
||||
env:
|
||||
IMAGE_VERSION: 4.0.0-beta
|
||||
TAG_SUFFIX: -$(date +%Y%m%d)-${GITHUB_SHA::7}
|
||||
jobs:
|
||||
build:
|
||||
name: Builds Docker images
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
service:
|
||||
- name: openslides-proxy
|
||||
directory: proxy
|
||||
|
||||
- name: openslides-client
|
||||
directory: openslides-client
|
||||
|
||||
- name: openslides-backend
|
||||
directory: openslides-backend
|
||||
|
||||
- name: openslides-datastore-reader
|
||||
directory: openslides-datastore-service
|
||||
args:
|
||||
MODULE: reader
|
||||
PORT: 9010
|
||||
|
||||
- name: openslides-datastore-writer
|
||||
directory: openslides-datastore-service
|
||||
args:
|
||||
MODULE: writer
|
||||
PORT: 9011
|
||||
|
||||
- name: openslides-autoupdate
|
||||
directory: openslides-autoupdate-service
|
||||
|
||||
- name: openslides-auth
|
||||
directory: openslides-auth-service
|
||||
|
||||
- name: openslides-vote
|
||||
directory: openslides-vote-service
|
||||
|
||||
- name: openslides-icc
|
||||
directory: openslides-icc-service
|
||||
|
||||
- name: openslides-media
|
||||
directory: openslides-media-service
|
||||
|
||||
- name: openslides-manage
|
||||
directory: openslides-manage-service
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Build image
|
||||
working-directory: ${{ matrix.service.directory }}
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
run: |
|
||||
if [ "${{ matrix.service.name }}" = "openslides-client" ]
|
||||
then
|
||||
eval echo ${IMAGE_VERSION}${TAG_SUFFIX} > client/src/assets/version.txt
|
||||
fi
|
||||
if [ "${{ matrix.service.args }}" != "" ]
|
||||
then
|
||||
export BUILD_ARGS="--build-arg MODULE=${{ matrix.service.args.MODULE }}
|
||||
--build-arg PORT=${{ matrix.service.args.PORT }}"
|
||||
fi
|
||||
docker build . --tag ${{ matrix.service.name }} $BUILD_ARGS
|
||||
|
||||
- name: Log into registry
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" |
|
||||
docker login ghcr.io --username ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Push image
|
||||
run: |
|
||||
IMAGE_ID=ghcr.io/${{ github.repository }}/${{ matrix.service.name }}
|
||||
|
||||
# Change all uppercase to lowercase
|
||||
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
|
||||
|
||||
docker tag ${{ matrix.service.name }} ${IMAGE_ID}:$(eval echo ${IMAGE_VERSION}${TAG_SUFFIX})
|
||||
docker tag ${{ matrix.service.name }} ${IMAGE_ID}:latest
|
||||
docker push ${IMAGE_ID}:$(eval echo ${IMAGE_VERSION}${TAG_SUFFIX})
|
||||
docker push ${IMAGE_ID}:latest
|
|
@ -0,0 +1,133 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test-server:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
USING_COVERAGE: "3.6,3.8"
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.6", "3.7", "3.8"]
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./server
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: "${{ matrix.python-version }}"
|
||||
|
||||
- name: install python dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install --upgrade --requirement requirements.txt
|
||||
pip freeze
|
||||
|
||||
- name: lint with flake8
|
||||
run: flake8 openslides tests
|
||||
|
||||
- name: lint with isort
|
||||
run: isort --check-only --diff --recursive openslides tests
|
||||
|
||||
- name: lint with black
|
||||
run: black --check --diff openslides tests
|
||||
|
||||
- name: test using mypy
|
||||
run: mypy openslides/ tests/
|
||||
|
||||
- name: test using pytest
|
||||
run: pytest --cov --cov-fail-under=73
|
||||
|
||||
install-client-dependencies:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./client
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
id: node-module-cache
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: node_modules-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
node_modules-
|
||||
- uses: actions/setup-node@v2-beta
|
||||
with:
|
||||
node-version: "14"
|
||||
|
||||
- name: install client dependencies
|
||||
if: steps.node-module-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
|
||||
check-client-code:
|
||||
needs: install-client-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./client
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: node_modules-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
- name: check code using linter
|
||||
run: npm run lint-check
|
||||
|
||||
- name: check code using prettify
|
||||
run: npm run prettify-check
|
||||
|
||||
test-client:
|
||||
needs: install-client-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./client
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: node_modules-${{ hashFiles('**/package-lock.json') }}
|
||||
- name: test client
|
||||
run: npm run test-silently
|
||||
|
||||
build-client-debug:
|
||||
needs: install-client-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./client
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: node_modules-${{ hashFiles('**/package-lock.json') }}
|
||||
- name: build client debug
|
||||
run: npm run build-debug
|
||||
|
||||
build-client-prod:
|
||||
needs: install-client-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./client
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: node_modules-${{ hashFiles('**/package-lock.json') }}
|
||||
- name: build client prod
|
||||
run: npm run build
|
|
@ -1,25 +0,0 @@
|
|||
---
|
||||
name: Run integration tests (cypress)
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
run-cypress:
|
||||
name: 'Runs integration tests in cypress'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Build and run OS4 Dev
|
||||
run: make run-dev ARGS="-d"
|
||||
|
||||
- name: Run integration tests (cypress)
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
working-directory: integration
|
||||
wait-on: 'https://localhost:8000'
|
||||
wait-on-timeout: 300
|
||||
env:
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
|
@ -1,49 +1,87 @@
|
|||
# General
|
||||
## General
|
||||
*.pyc
|
||||
*.swp
|
||||
*.swo
|
||||
*.log
|
||||
*~
|
||||
.DS_Store
|
||||
.idea
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
.vscode/*
|
||||
*.code-workspace
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
# Virtual Environment
|
||||
.virtualenv*
|
||||
.venv
|
||||
|
||||
# docs
|
||||
docs/modelsvalidator/modelsvalidator
|
||||
dev-commands/export.json
|
||||
|
||||
# certs
|
||||
*.pem
|
||||
|
||||
# Deployment
|
||||
/docker/docker-compose.yml
|
||||
/docker/docker-stack.yml
|
||||
## Compatibility
|
||||
# OS4-Submodules and aux-directories
|
||||
/openslides-*/
|
||||
/docker/keys/
|
||||
/docker/secrets/auth_*_key
|
||||
docker/secrets/*.env
|
||||
|
||||
# Integration testing
|
||||
/integration/results
|
||||
/integration/cypress/downloads
|
||||
/integration/cypress/screenshots
|
||||
/integration/cypress/videos
|
||||
/integration/node_modules
|
||||
|
||||
# Old OS3 files and folders
|
||||
.coverage
|
||||
.mypy_cache
|
||||
Compodoc
|
||||
__pycache__
|
||||
bower_components
|
||||
client
|
||||
make
|
||||
openslides_*
|
||||
openslides
|
||||
personal_data
|
||||
tests
|
||||
.launch/
|
||||
.venv/
|
||||
.virtualenv/
|
||||
.vscode/
|
||||
client/package-lock.json
|
||||
server/
|
||||
/docs/
|
||||
# OS3+-Submodules
|
||||
/autoupdate/
|
||||
# Plugin development
|
||||
openslides_*
|
||||
# Old OS3 stuff
|
||||
/tests/
|
||||
|
||||
## Server
|
||||
# Local user data (settings, database, media, search index, static files)
|
||||
personal_data/*
|
||||
server/personal_data/*
|
||||
server/openslides/static/*
|
||||
# Unit test and coverage reports
|
||||
.coverage
|
||||
server/tests/file/*
|
||||
server/tests/db.sqlite3.test
|
||||
.pytest_cache
|
||||
# Package building
|
||||
*.egg-info
|
||||
# Mypy cache for typechecking
|
||||
.mypy_cache
|
||||
|
||||
## OpenSlides 3 Client
|
||||
# Javascript tools and libraries
|
||||
**/node_modules/*
|
||||
**/bower_components/*
|
||||
# compiled output
|
||||
client/dist
|
||||
client/static
|
||||
client/tmp
|
||||
client/out-tsc
|
||||
# docs
|
||||
client/documentation
|
||||
Compodoc
|
||||
Compodocmodules
|
||||
CHANGELOG.md
|
||||
# build artifacts
|
||||
client/.sass-cache
|
||||
client/connect.lock
|
||||
client/coverage
|
||||
client/libpeerconnection.log
|
||||
client/npm-debug.log
|
||||
client/yarn-error.log
|
||||
client/testem.log
|
||||
client/typings
|
||||
client/yarn.lock
|
||||
package-lock.json
|
||||
client/package-lock.json
|
||||
cypress.json
|
||||
|
||||
## Deployment
|
||||
# Docker build artifacts
|
||||
docker/docker-compose.yml
|
||||
docker/docker-stack.yml
|
||||
*-version.txt
|
||||
*.pem
|
||||
# secrets
|
||||
docker/secrets/*.env
|
||||
|
|
|
@ -1,36 +1,3 @@
|
|||
[submodule "openslides-datastore-service"]
|
||||
path = openslides-datastore-service
|
||||
url = https://github.com/OpenSlides/openslides-datastore-service.git
|
||||
branch = main
|
||||
[submodule "openslides-client"]
|
||||
path = openslides-client
|
||||
url = https://github.com/OpenSlides/openslides-client.git
|
||||
branch = main
|
||||
[submodule "openslides-backend"]
|
||||
path = openslides-backend
|
||||
url = https://github.com/OpenSlides/openslides-backend.git
|
||||
branch = main
|
||||
[submodule "openslides-autoupdate-service"]
|
||||
path = openslides-autoupdate-service
|
||||
url = https://github.com/OpenSlides/openslides-autoupdate-service.git
|
||||
branch = main
|
||||
[submodule "openslides-auth-service"]
|
||||
path = openslides-auth-service
|
||||
url = https://github.com/OpenSlides/openslides-auth-service.git
|
||||
branch = main
|
||||
[submodule "openslides-media-service"]
|
||||
path = openslides-media-service
|
||||
url = https://github.com/OpenSlides/openslides-media-service.git
|
||||
branch = main
|
||||
[submodule "openslides-manage-service"]
|
||||
path = openslides-manage-service
|
||||
url = https://github.com/OpenSlides/openslides-manage-service.git
|
||||
branch = main
|
||||
[submodule "openslides-icc-service"]
|
||||
path = openslides-icc-service
|
||||
url = https://github.com/OpenSlides/openslides-icc-service.git
|
||||
branch = main
|
||||
[submodule "openslides-vote-service"]
|
||||
path = openslides-vote-service
|
||||
url = https://github.com/OpenSlides/openslides-vote-service.git
|
||||
branch = main
|
||||
[submodule "autoupdate"]
|
||||
path = autoupdate
|
||||
url = https://github.com/OpenSlides/openslides3-autoupdate-service.git
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
Advanced configuration
|
||||
======================
|
||||
|
||||
Docker Swarm Mode
|
||||
-----------------
|
||||
|
||||
OpenSlides may also be deployed in Swarm mode. Distributing instances over
|
||||
multiple nodes may increase performance and offer failure resistance.
|
||||
|
||||
An example configuration file, ``docker-stack.yml.m4``, is provided. Unlike
|
||||
the Docker Compose setup, this configuration will most likely need to be
|
||||
customized, especially its placement constraints and database-related
|
||||
preferences.
|
||||
|
||||
Before deploying an instance on Swarm, please see `Database Configuration`_ and
|
||||
`Backups`_, and review your ``docker-stack.yml``
|
||||
|
||||
|
||||
Database Configuration
|
||||
----------------------
|
||||
|
||||
It is fairly easy to get an OpenSlides instance up an running; however, for
|
||||
production setups it is strongly advised to review the database configuration.
|
||||
|
||||
By default, the primary database cluster will archive all WAL files in its
|
||||
volume. Regularly pruning old data is left up to the host system, i.e., you.
|
||||
Alternatively, you may disable WAL archiving by setting
|
||||
``PGNODE_WAL_ARCHIVING=off`` in ``.env`` before starting the instance.
|
||||
|
||||
The provided ``docker-stack.yml.m4`` file includes additional database
|
||||
services which can act as hot standby clusters with automatic failover
|
||||
functionality. To take advantage of this setup, the database services need to
|
||||
be configured with proper placement constraints. Before relying on this setup,
|
||||
please familiarize yourself with `repmgr <https://repmgr.org/>`_.
|
||||
|
||||
|
||||
Backups
|
||||
-------
|
||||
|
||||
All important data is stored in the database. Additionally, the project
|
||||
directory should be included in backups to ensure a smooth recovery.
|
||||
|
||||
The primary database usually runs in the ``pgnode1`` service (but see `Database
|
||||
Configuration`_ above).
|
||||
|
||||
In some cases, it may be sufficient to generate SQL dumps with ``pg_dump``
|
||||
through ``docker exec`` to create backups. However, for proper incremental
|
||||
backups, the host system can backup the cluster's data directory and WAL
|
||||
archives.
|
||||
|
||||
The cluster's data directory is available as a volume on the host system.
|
||||
Additionally, the database archives its WAL files in the same volume by
|
||||
default. This way, the host system can include the database volume in its
|
||||
regular filesystem-based backup routine and create efficient database backups
|
||||
suitable for point-in-time recovery.
|
||||
|
||||
The `former management repository
|
||||
<https://github.com/OpenSlides/openslides-docker-compose/>`_ provides the
|
||||
script `openslides-pg-mgr.sh` which can enable Postgres' backup mode in all
|
||||
OpenSlides database containers.
|
||||
|
||||
In Swarm mode, the primary database cluster may get placed on a number of
|
||||
nodes. It is, therefore, crucial to restrict the placement of database
|
||||
services to nodes on which appropriate backups have been configured.
|
9
AUTHORS
9
AUTHORS
|
@ -30,10 +30,7 @@ Authors of OpenSlides in chronological order of first contribution:
|
|||
Jochen Saalfeld <jochen.saalfeld@intevation.de>
|
||||
Fadi Abbud <fmfn13@hotmail.com>
|
||||
Gabriel Meyer <meyergabriel@live.de>
|
||||
Gernot Schulz <gernot@intevation.de>
|
||||
Joshua Sangmeister <joshua.sangmeister@gmail.com>
|
||||
Ralf Peschke <rpeschke@peschke-it.de>
|
||||
Ludwig Reiter <ludwig.reiter@intevation.de>
|
||||
Adrian Richter <adrian@intevation.de>
|
||||
Camille Akmut
|
||||
Johannes Rolf <johannes.rolf@rwth-aachen.de>
|
||||
Gernot Schulz <gernot@intevtion.de>
|
||||
Raphael Topel <info@rtopel.de>
|
||||
Martin Dickopp <martin@zero-based.org>
|
||||
|
|
964
CHANGELOG.md
964
CHANGELOG.md
|
@ -1,964 +0,0 @@
|
|||
# CHANGELOG of OpenSlides
|
||||
|
||||
https://openslides.com
|
||||
|
||||
|
||||
## Version 3.1 (2019-12-13)
|
||||
|
||||
[Milestone](https://github.com/OpenSlides/OpenSlides/milestones/3.0)
|
||||
|
||||
**General:**
|
||||
- Improved loading time of OpenSlides [#5061, #5087, #5110, #5146 - Breaks IE11].
|
||||
- Improved Websocket reconnection - works now more reliable [#5060].
|
||||
- Improved performance by replacing deprecated encode and decode library with faster manual approaches [#5085, #5092].
|
||||
- Improved mobile views for small devices [#5106].
|
||||
- Improved virtual scrolling behavior of tables: remember last scroll position [#5156].
|
||||
- New SingleSignOn login method via SAML [#5000].
|
||||
- New inline editing for start page, legal notice and privacy policy [#5086].
|
||||
- Reordered settings in subpages for better overview [#4878, #5083, #5096].
|
||||
- Added html meta noindex tag to prevent search engines finding instances of OpenSlides [#5061].
|
||||
- Added server log entries for usage of notify feature [#5066].
|
||||
- Added server-side HTML validation for personal notes (motions) and about me field (users) [#5121].
|
||||
- Fixed an issue where projector button in lists was always not indicating the projected element [#5055].
|
||||
- Fixed issues where search-filter, property-filter and property-sort in list views does not work correctly [#5065].
|
||||
- Fixed an issues where list view entries using virtual scrolling become invisible [#5072].
|
||||
- Fixed an error where loading spinner would not disappear while offline mode [#5151].
|
||||
- Various cleanups and improvements to usability, performance and translation.
|
||||
|
||||
**Agenda:**
|
||||
- New config option to show motion submitters as subtitle in agenda list [#5002, #5094].
|
||||
- New view to sort sepakers of a list - preventing unwanted changes esp. using mobile devices [#5069].
|
||||
- New button to readd the last speaker to the top of the list [#5069, #5067].
|
||||
- New agenda list filter 'item type' (topic, motion, motion block, election) [#5084].
|
||||
- Changed window title for current list of speakers [#5037].
|
||||
- Added motion title in agenda list [#5063].
|
||||
|
||||
**Motions:**
|
||||
- New option to export personal notes [#5075].
|
||||
- New amendment filter for motion list [#5056, #5157].
|
||||
- New possibility to change state and recommendation in motion list using quick edit [#5071].
|
||||
- Added multi select actions to amendment list [#5041].
|
||||
- Added search value selector in multi select action dialogs [#5058].
|
||||
- Added support for nested lists with line numbers in PDF export [#5138].
|
||||
- Improved scaling of motion tile view [#5038].
|
||||
- Improved performance for large motions with dozens of amendments by implementing manual change detection in motion detail [#5074, #5139].
|
||||
- Improved display of long names for states and recommendations in drop down menu in motion detail view [#5079].
|
||||
- Improved amendment slide by showing only changed line(s)s without surrounding paragraph [#5144].
|
||||
- Fixed line number errors during creation of amendments [#5023].
|
||||
- Fixed an issue that ol/ul lists are not printed in amendment PDF [#5051].
|
||||
- Fixed the amendment option "Show entire motion text" [#5052].
|
||||
- Fixed a rare bug in final version where change recommendations or amendments would have been hidden [#5070].
|
||||
- Fixed PDF export in final version: use modified final version if available [#5139].
|
||||
- Fixes a bug where the event name was printed twice in the PDF header [#5155].
|
||||
|
||||
**Elections:**
|
||||
- Fixed errors by entering votes and sorting candidates [#5129, #5125].
|
||||
- Fixed a permission issue that prevented nominating another participants for elections [#5154].
|
||||
|
||||
**Users:**
|
||||
- Fixed wrong permission check for set password [#5128].
|
||||
|
||||
**Mediafiles:**
|
||||
- Fixed mediafile upload path [#4710].
|
||||
- Fixed double slash in mediafile URL [#5031].
|
||||
- Original filename must now be unique for files [#5123].
|
||||
|
||||
**Projector:**
|
||||
- New projector edit dialog with preview [#5043].
|
||||
- Added support for custom aspect ratios in projector edit dialog; database migration required [#5141].
|
||||
- Added missing autoupdates for changed projection defaults [#5045].
|
||||
- Added scaleable tile for projector list [#5043].
|
||||
- Added a lock icon on the list of speaker slide if list has been closed [#5154].
|
||||
- Improved autoupdates for projectors by using changeIDs [#5064].
|
||||
- Improved performance by preventing high CPU usage on Firefox in projector detail view [#5022].
|
||||
- Changed config option to show nice horizontal meta box on motion slide [#5088].
|
||||
- Changed config option "Event date" back to string format [#5042].
|
||||
- Saved countdown settings [#5053].
|
||||
|
||||
**Breaking Changes:**
|
||||
- Due to faster model handling (using the 'Proxy' function) Internet Explorer 11 cannot be supported anymore!
|
||||
|
||||
|
||||
## Version 3.0 (2019-09-13)
|
||||
|
||||
[Milestone](https://github.com/OpenSlides/OpenSlides/milestones/3.0)
|
||||
|
||||
**General (Client):**
|
||||
- OpenSlides client completely rewritten, based on Angular 8 and Material Design.
|
||||
- OpenSlides is now a Progressive Web App (PWA).
|
||||
- New browser caching via IndexedDB (one cache store for all browser tabs).
|
||||
- New list views optimized with virtual scrolling (improved performance for long lists).
|
||||
- New global quick search using by shortcut 'Alt+Shift+F'.
|
||||
- New built-in design themes for customizing user interface.
|
||||
- New update notification if OpenSlides static files are updated.
|
||||
- New config option for pdf page size (DIN A4 or A5).
|
||||
- Added TinyMCE 5 editor (switched from CKEditor caused by changed license).
|
||||
- Switched from yarn/gulp to npm.
|
||||
- Improved pdf gerneration with progress bar and cancel option.
|
||||
- Translations available for EN, DE, RU and CS.
|
||||
|
||||
**General (Server):**
|
||||
- New websocket protocol for server client communication using JSON schema.
|
||||
- New change-id system to send only updated elements to client.
|
||||
- New global history mode (useable for admin group only).
|
||||
- Updated to Channels 2.
|
||||
- Dropped support for Python 3.5.
|
||||
- Dropped support for Geiss.
|
||||
- Complete rework of startup and caching system. Dropped restricted data cache.
|
||||
- Changed URL schema.
|
||||
- Changed personal settings.py.
|
||||
- Changed format for elements send via autoupdate.
|
||||
- Changed projector concept.
|
||||
- Compressed autoupdates before sending to clients (reduced traffic).
|
||||
- Fixed autoupdate system for related objects.
|
||||
- Fixed logo configuration if logo file is deleted.
|
||||
- Added several bulk views for motions and users (one request for updating multiple selected elements).
|
||||
- Added docs for using OpenSlides in 'big mode' with Gunicorn and Uvicorn.
|
||||
- Added docs for configure OpenSlides in settingy.py.
|
||||
- Dropped chat functionality.
|
||||
- Server performance improvements.
|
||||
|
||||
**Agenda:**
|
||||
- Agenda items are now optional (for motions, elections and mediafiles). New config to set default behavior.
|
||||
- New drag&drop view to sort agenda items.
|
||||
- New config option: only present participants can be added to list of speakers.
|
||||
- New config option to hide number of speakers on projector.
|
||||
|
||||
**Motions:**
|
||||
- New call list for custom sort of motions.
|
||||
- New tile layout view with all categories (each category a tile).
|
||||
- New statute motions with managing statute paragraphs.
|
||||
- New permission to manage metadata (state, recommendation, submitters and supporters, category, motion block and polls).
|
||||
- New permission to create amendments.
|
||||
- New permission to see motions in internal states.
|
||||
- New access restrictions definable for each motion state in workflow.
|
||||
- New 'internal' option for motion blocks.
|
||||
- New sorting view for categories to create subcategories.
|
||||
- New custom comment fields for all motions (read/write access can be managed via permission groups).
|
||||
- New motion history (each action is stored in global OpenSlides history which can be restored any time, replaced old motion version and log features).
|
||||
- New XLSX export (docx support is dropped).
|
||||
- New navigation for next/previous motion in detail view (shortcut: 'Alt+Shift+Left/Right').
|
||||
- New multi select actions.
|
||||
- New timestampes for motions (for sorting by creation date and last modified).
|
||||
- New config option to set reason as required field.
|
||||
- New config option to change multiple paragraphs with an amendment.
|
||||
- New config option to hide motion text on projector.
|
||||
- New config option to show sequential number.
|
||||
- New config option to show all motions which are referred to a special motion.
|
||||
- New config option to show submitters and recommendation in table of contents of PDF.
|
||||
- New config options to control identifier generation - number of digits and blanks (moved from settings.py).
|
||||
- Improved PDF export (optional with toc, page numbers, date, comments and meta information)
|
||||
- Improved motion numbering in (sub)categories: Motions of subcategories are also numbered, and parents of amendments needs to be in the numbered category or any subcategory.
|
||||
- Improved projection layout of motion blocks.
|
||||
- Changed default workflows: Allowed submitters to set state of new motions in complex and customized workflow. No migration provided.
|
||||
- Change CSV import to add tags.
|
||||
|
||||
**User:**
|
||||
- New admin group which grants all permissions. Users of existing group 'Admin' or 'Staff' are move to the new group during migration.
|
||||
- New gender field.
|
||||
- New password forget/reset function via email.
|
||||
- New permission to change own password.
|
||||
- New config option for sender name and reply email address (From address is defined in settings.py).
|
||||
|
||||
**Mediafiles:**
|
||||
- New support for (sub)folders and permission groups.
|
||||
|
||||
**Projector:**
|
||||
- New views to list, manage and control created OpenSlides projectors.
|
||||
- New projector queue (add slide to queue), all projected slides are logged.
|
||||
- New chyron for current speaker.
|
||||
- New color settings for each projector.
|
||||
|
||||
|
||||
## Version 2.3 (2018-09-20)
|
||||
[Release notes](https://github.com/OpenSlides/OpenSlides/wiki/OpenSlides-2.3) · [Milestone](https://github.com/OpenSlides/OpenSlides/milestones/2.3)
|
||||
|
||||
**Agenda:**
|
||||
- New item type 'hidden'. New visibilty filter in agenda [#3790].
|
||||
|
||||
**Motions:**
|
||||
- New feature to scroll the projector to a specific line [#3748].
|
||||
- New possibility to sort submitters [#3647].
|
||||
- New representation of amendments (paragraph based creation, new diff and list views for amendments) [#3637].
|
||||
- New feature to customize workflows and states [#3772, #3785].
|
||||
- New table of contents with page numbers and categories in PDF [#3766].
|
||||
- New teporal field "modified final version" where the final version can be edited [#3781].
|
||||
- New config options to show logos on the right side in PDF [#3768].
|
||||
- New config to show amendments also in motions table [#3792].
|
||||
- Support to change decimal places for polls with a plugin [#3803].
|
||||
|
||||
**Elections:**
|
||||
- Support to change decimal places for elections with a plugin [#3803]
|
||||
|
||||
**Core:**
|
||||
- Updated Django to 2.1 [#3777, #3786].
|
||||
- Support for Python 3.7 [#3786].
|
||||
- Python 3.4 is not supported anymore [#3777].
|
||||
- Updated pdfMake to 0.1.37 [#3766].
|
||||
- Changed behavior of collectstatic management command [#3804].
|
||||
|
||||
|
||||
## Version 2.2 (2018-06-06)
|
||||
|
||||
[Release notes](https://github.com/OpenSlides/OpenSlides/wiki/OpenSlides-2.2) · [Milestone](https://github.com/OpenSlides/OpenSlides/milestones/2.2)
|
||||
|
||||
**Agenda:**
|
||||
- New permission for managing lists of speakers [#3366].
|
||||
- New DOCX export of agenda [#3569].
|
||||
- New collapsable agenda overview [#3567].
|
||||
- New feature: mark speakers (e.g. as submitter) [#3570].
|
||||
- New config option to enable numbering of items [#3697].
|
||||
- New config option to hide internal items when projecting subitems [#3701].
|
||||
- Hide closed agenda items in the item slide [#3567].
|
||||
- Fixed wrong sorting of last speakers [#3193].
|
||||
- Fixed issue when sorting a new inserted speaker [#3210].
|
||||
- Fixed multiple request on creation of agenda related items [#3341].
|
||||
- Autoupdates for all children if the item type has changed [#3659].
|
||||
|
||||
**Motions:**
|
||||
- New export dialog for managers only [#3185].
|
||||
- New personal note field for each motions [#3190, #3267, #3404].
|
||||
- New navigation between single motions [#3459].
|
||||
- New possibility to create change recommendations for motion titles [#3626].
|
||||
- New support for export motions in a ZIP archive [#3189, #3251].
|
||||
- New PDF export for personal note and comments [#3239].
|
||||
- New config option for customize sorting of category list in pdf/docx export [#3329].
|
||||
- New config optoin for pagenumber alignment in PDF [#3327].
|
||||
- New config options to hide reason, recommendation and meta data box on projector [#3432, #3692].
|
||||
- New inline editing for motion reason [#3361].
|
||||
- New multiselect filter for motion comments [#3372].
|
||||
- New support for pinning personal notes to the window [#3360].
|
||||
- New warning message if an edit dialog was already opened by another client [#3212].
|
||||
- New change recommendation type "other" [#3495].
|
||||
- Fixed issue when creating/deleting motion comment fields in the settings [#3187].
|
||||
- Fixed empty motion comment field in motion update form [#3194].
|
||||
- Fixed error on category sort [#3318].
|
||||
- Bugfix: Changing motion line length did not invalidate cache [#3202].
|
||||
- Bugfix: Added more distance in motion PDF for DEL-tags in new lines [#3211].
|
||||
- Bugfix: Creating colliding change recommendation is now prevented on server side [#3304].
|
||||
- Bugfix: Several bugfixes regarding splitting list items in change recommendations [#3288].
|
||||
- Bugfix: Several bugfixes regarding diff version [#3407, #3408, #3410, #3440, #3450, #3465, #3537, #3546, #3548, #3644, #3656].
|
||||
- Improved the multiselect state filter [#3459].
|
||||
- Save pagination state to session storage [#3569].
|
||||
- Allow to delete own motions [#3516].
|
||||
- Reference to motions by id in state and recommendation special field [#3498].
|
||||
- Log which comment was updated [#3569].
|
||||
- Split up 'can_see_and_manage_comments' permission in two seperate ones [#3565].
|
||||
- Combined all boolean filters into one dropdown menu and added a filter for amendments [#3501].
|
||||
- Show motion identifier in (current) list of speakers [#3442]
|
||||
- Show the number of next speakers in motion list view [#3470].
|
||||
- Added (shortened) motion title to motion block slide [#3700].
|
||||
- Clear identifier on state reset [#3356].
|
||||
- Reworked DOCX export parser and added comments to DOCX [#3258].
|
||||
- Removed server side image to base64 transformation and added local transformation [#3181].
|
||||
- Added karma:watch command [#3466].
|
||||
|
||||
**Elections:**
|
||||
- New pagination for list view [#3393].
|
||||
|
||||
**Users:**
|
||||
- New fast mass import for users [#3290].
|
||||
- New default user group 'admin' [#3621].
|
||||
- New feature to send invitation emails with OpenSlides login data [#3503, #3525].
|
||||
- New view to toggle presence by entering participant number (can be used with barcode scanner) [#3496].
|
||||
- New support for password validation using Django or custom validators
|
||||
5. 7. for minimum password length [#3200].
|
||||
- Hide password in change password view [#3417].
|
||||
- Users without the permission 'can see users' can now see agenda item speakers, motion submitters and supporters, assignment candidates, mediafile uploader and chat message users if they have the respective permissions [#3191, #3233].
|
||||
- Fixed compare of duplicated users while CSV user import [#3201].
|
||||
- Added settings option to enable updating the last_login field in the database. The default is now disabled [#3400].
|
||||
- Removed OPTIONS request. All permissions are now provided on startup [#3306].
|
||||
|
||||
**Mediafiles:**
|
||||
- New form for uploading multiple files [#3650].
|
||||
- New custom CKEditor plugin for browsing mediafiles [#3337].
|
||||
- Project images always in fullscreen [#3355].
|
||||
- Protect mediafiles for forbidden access [#3384].
|
||||
- Fixed reloading of PDF on page change [#3274].
|
||||
|
||||
**Core:**
|
||||
- New settings to upload custom fonts (for projector and pdf) [#3568].
|
||||
- New custom translations to use custom wordings [#3383].
|
||||
- New support for choosing image files as logos for projector, PDF and web interface header [#3184, #3207, #3208, #3310].
|
||||
- New notify system [#3212].
|
||||
- New config option for standard font size in PDF [#3332].
|
||||
- New config option for disabling header and footer in the projector [#3357].
|
||||
- New dynamic webpage title [#3404].
|
||||
- New 'go to top'-link [#3404].
|
||||
- New custom format cleanup plugin for CKEditor [#3576].
|
||||
- Reset scroll level for each new projection [#3686].
|
||||
- Scroll to top on every state change [#3689].
|
||||
- Added pagination on top of lists [#3698].
|
||||
- Improved performance for PDF generation significantly (by upgrading to pdfmake 0.1.30) [#3278, #3285].
|
||||
- Enhanced performance esp. for server restart and first connection of all clients by refactoring autoupdate, Collection and AccessPermission [#3223, #3539].
|
||||
- Improved reconnect handling if the server was flushed [#3297].
|
||||
- No reload on logoff. OpenSlides is now a full single page application [#3172].
|
||||
- Highlight list entries in a light blue, if a related object is projected (e. g. a list of speakers of a motion) [#3301].
|
||||
- Select the projector resolution with a slider and an aspect ratio [#3311].
|
||||
- Delay the 'could not load projector' error 3 seconds to not irritate users with a slow internet connection [#3323].
|
||||
- Added default sorting for agenda, motions, elections, mediafiles and users [#3334, 3348].
|
||||
- Added caching for the index views [#3419, #3424].
|
||||
- Added projector prioritization [#3425].
|
||||
- Added --debug-email flag to print all emails to stdout [#3530].
|
||||
- Added --no-template-caching flag to disable template caching for easier development [#3566].
|
||||
- Updated CKEditor to 4.7 [#3375].
|
||||
- Reduced ckeditor toolbar for inline editing [#3368].
|
||||
- New api route to project items with just one request needed [#3713].
|
||||
- Use native twisted mode for daphne [#3487].
|
||||
- Saved language selection to session storage [#3543].
|
||||
- Set default of projector resolution to 1220x915 [#2549].
|
||||
- Preparations for the SAML plugin; Fixed caching of main views [#3535].
|
||||
- Removed unnecessary OPTIONS request in config [#3541].
|
||||
- Switched from npm to Yarn [#3188].
|
||||
- Improvements for plugin integration [#3330].
|
||||
- Cleanups for the collection and autoupdate system [#3390]
|
||||
- Bugfixes for PDF creation [#3227, #3251, #3279, #3286, #3346, #3347, #3342].
|
||||
- Fixed error when clearing empty chat [#3199].
|
||||
- Fixed autoupdate bug for a user without user.can_see_name permission [#3233].
|
||||
- Fixed bug the elements are projected and the deleted [#3336].
|
||||
- Several bugfixes and minor improvements.
|
||||
|
||||
*[#xxxx] = Pull request number to get more details on https://github.com/OpenSlides/OpenSlides/pulls*
|
||||
|
||||
|
||||
## Version 2.1.1 (2017-04-05)
|
||||
|
||||
[Milestone](https://github.com/OpenSlides/OpenSlides/milestones/2.1.1)
|
||||
|
||||
**Agenda:**
|
||||
- Fixed issue #3173 that the agenda item text cannot be changed.
|
||||
|
||||
**Other:**
|
||||
- Set required version for optional Geiss support to <1.0.0.
|
||||
|
||||
|
||||
## Version 2.1 (2017-03-29)
|
||||
|
||||
[Release notes](https://github.com/OpenSlides/OpenSlides/wiki/OpenSlides-2.1) · [Milestone](https://github.com/OpenSlides/OpenSlides/milestones/2.1)
|
||||
|
||||
**Agenda:**
|
||||
- Added button to remove all speakers from a list of speakers.
|
||||
- Added option to create or edit agenda items as subitems of others.
|
||||
- Fixed security issue: Comments were shown for unprivileged users.
|
||||
- Added option to choose whether to show the current list of speakers slide as a slide or an overlay.
|
||||
- Manage speakers on the current list of speakers view.
|
||||
- List of speakers for hidden items is always visible.
|
||||
|
||||
**Core:**
|
||||
- Added support for multiple projectors.
|
||||
- Added control for the resolution of the projectors.
|
||||
- Added smooth projector scroll.
|
||||
- Set the projector language in the settings.
|
||||
- Added migration path from OpenSlides 2.0.
|
||||
- Added support for big assemblies with lots of users.
|
||||
- Django 1.10 is now supported. Dropped support for Django 1.8 and 1.9.
|
||||
- Used Django Channels instead of Tornado. Refactoring of the autoupdate process. Added retry with timeout in case of ChannelFull exception.
|
||||
- Made a lot of autoupdate improvements for projector and site.
|
||||
- Added new caching system with support for Redis.
|
||||
- Support https as websocket protocol (wss).
|
||||
- Accelerated startup process (send all data to the client after login).
|
||||
- Add the command getgeiss to download the latest version of Geiss.
|
||||
- Add a version of has_perm that can work with cached users.
|
||||
- Removed our AnonymousUser. Make sure not to use user.has_perm() anymore.
|
||||
- Added function utils.auth.anonymous_is_enabled which returns true, if it is.
|
||||
- Changed has_perm to support an user id or None (for anyonmous) as first argument.
|
||||
- Cache the group with there permissions.
|
||||
- Added watching permissions in client and change the view immediately on changes.
|
||||
- Used session cookies and store filter settings in session storage.
|
||||
- Removed our db-session backend and added possibility to use any django session backend.
|
||||
- Added template hook system for plugins.
|
||||
- Used Roboto font in all templates.
|
||||
- Added HTML support for messages on the projector.
|
||||
- Moved custom slides to own app "topics". Renamed it to "Topic".
|
||||
- Added button to clear the chatbox.
|
||||
- Better dialog handling. Show dialog just in forground without changing the state url. Added new dialog for profile, change password, tag and category update view.
|
||||
- Switched editor back from TinyMCE to CKEditor which provides a better copy/paste support from MS Word.
|
||||
- Validate HTML strings from CKEditor against XSS attacks.
|
||||
- Use a separate dialog with CKEditor for editing projector messages.
|
||||
- Use CKEditor in settings for text markup.
|
||||
- Used pdfMake for clientside generation of PDFs. Run pdf creation in background (in a web worker thread).
|
||||
- Introduced new table design for list views with serveral filters and CSV export.
|
||||
- New CSV import layout.
|
||||
- Replaced angular-csv-import by Papa Parse for CSV parsing.
|
||||
- Added UTF-8 byte order mark for every CSV export.
|
||||
- Removed config cache to support multiple threads or processes.
|
||||
- Added success/error symbol to config to show if saving was successful.
|
||||
- Fixed bug, that the last change of a config value was not send via autoupdate.
|
||||
- Moved full-text search to client-side (removed the server-side search engine Whoosh).
|
||||
- Made a lot of code clean up, improvements and bug fixes in client and backend.
|
||||
|
||||
**Motions:**
|
||||
- Added adjustable line numbering mode (outside, inside, none) for each motion text.
|
||||
- Allowed to add change recommendations for special motion text lines (with diff mode).
|
||||
- Added projection support for change recommendations.
|
||||
- Added button to sort and number all motions in a category.
|
||||
- Added recommendations for motions.
|
||||
- Added options to calculate percentages on different bases.
|
||||
- Added calculation for required majority.
|
||||
- Added blocks for motions which can be used in agenda. Set states for multiple motions of a motion block by following the recommendation for each motion.
|
||||
- Used global config variable for preamble.
|
||||
- Added configurable fields for comments.
|
||||
- Added new origin field.
|
||||
- Reimplemented amendments.
|
||||
- New PDF layout.
|
||||
- Added DOCX export with docxtemplater.
|
||||
- Changed label of former state "commited a bill" to "refered to committee".
|
||||
- Number of ballots printed can now be set in config.
|
||||
- Add new personal settings to remove all whitespaces from motion identifier.
|
||||
- Add new personal settings to allow amendments of amendments.
|
||||
- Added inline editing for comments.
|
||||
|
||||
**Elections:**
|
||||
- Added options to calculate percentages on different bases.
|
||||
- Added calculation for required majority.
|
||||
- Candidates are now sortable.
|
||||
- Removed unused assignment config to publish winner election results only.
|
||||
- Number of ballots printed can now be set in config.
|
||||
- Added inline edit field for a specific hint on ballot papers.
|
||||
|
||||
**Users:**
|
||||
- Added new matrix-interface for managing groups and their permissions.
|
||||
- Added autoupdate on permission change (permission added).
|
||||
- Improved password reset view for administrators.
|
||||
- Changed field for initial password to an unchangeable field.
|
||||
- Added new field for participant number.
|
||||
- Added new field 'is_committee' and new default group 'Committees'.
|
||||
- Improved users CSV import (use group names instead of id).
|
||||
- Allowed to import/export initial user password.
|
||||
- Added more multiselect actions.
|
||||
- Added QR code in users access pdf.
|
||||
|
||||
**Mediafiles:**
|
||||
- Allowed to project uploaded images (png, jpg, gif) and video files (e. g. mp4, wmv, flv, quicktime, ogg).
|
||||
- Allowed to hide uploaded files in overview list for non authorized users.
|
||||
- Enabled removing of files from filesystem on model instance delete.
|
||||
|
||||
**Other:**
|
||||
- Added Russian translation (Thanks to Andreas Engler).
|
||||
- Added command to create example data.
|
||||
|
||||
|
||||
## Version 2.0 (2016-04-18)
|
||||
|
||||
[Milestone](https://github.com/OpenSlides/OpenSlides/milestones/2.0)
|
||||
|
||||
*OpenSlides 2.0 is essentially not compatible to OpenSlides 1.7. E. g. customized templates, databases and plugins can not be reused without adaption.*
|
||||
|
||||
**Agenda:**
|
||||
- Updated the tests and changed internal parts of method of the agenda model.
|
||||
- Changed API of related objects. All assignments, motions and custom slides are now agenda items and can be hidden.
|
||||
- Removed django-mptt.
|
||||
- Added attachments to custom sldies.
|
||||
- Improved CSV import.
|
||||
|
||||
**Assignments:**
|
||||
- Renamed app from assignment to assignments.
|
||||
- Removed possibility to block candidates.
|
||||
- Massive refactoring and cleanup of the app.
|
||||
|
||||
**Motions:**
|
||||
- Renamed app from motion to motions.
|
||||
- Massive refactoring and cleanup of the app.
|
||||
|
||||
**Mediafiles:**
|
||||
- Renamed app from mediafile to mediafiles.
|
||||
- Used improved pdf presentation with angular-pdf.
|
||||
- Massive refactoring and cleanup of the app.
|
||||
|
||||
**Users:**
|
||||
- Massive refactoring of the participant app. Now called 'users'.
|
||||
- Used new anonymous user object instead of an authentification backend. Used special authentication class for REST requests.
|
||||
- Used authentication frontend via AngularJS.
|
||||
- Improved CSV import.
|
||||
|
||||
**Other:**
|
||||
- New OpenSlides logo.
|
||||
- New design for web interface.
|
||||
- Added multiple countdown support.
|
||||
- Added colored countdown for the last n seconds (configurable).
|
||||
- Switched editor from CKEditor to TinyMCE.
|
||||
- Changed supported Python version to >= 3.4.
|
||||
- Used Django 1.8 as lowest requirement.
|
||||
- Django 1.9 is supported
|
||||
- Added Django's application configuration. Refactored loading of signals and projector elements/slides.
|
||||
- Setup migrations.
|
||||
- Added API using Django REST Framework 3.x. Added several views and mixins for generic Django REST Framework views in OpenSlides apps.
|
||||
- Removed most of the Django views and templates.
|
||||
- Removed Django error pages.
|
||||
- Added page for legal notice.
|
||||
- Refactored projector API using metaclasses now.
|
||||
- Renamed SignalConnectMetaClass classmethod get_all_objects to get_all (private API).
|
||||
- Refactored config API and moved it into the core app.
|
||||
- Removed old style personal info page, main menu entries and widget API.
|
||||
- Used AngularJS with additional libraries for single page frontend.
|
||||
- Removed use of 'django.views.i18n.javascript_catalog'. Used angular-gettext now.
|
||||
- Updated to Bootstrap 3.
|
||||
- Used SockJS for automatic update of AngularJS driven single page frontend.
|
||||
- Refactored plugin API.
|
||||
- Refactored start script and management commands. Changed command line option and path for local installation.
|
||||
- Refactored tests.
|
||||
- Used Bower and gulp to manage third party JavaScript and Cascading Style Sheets libraries.
|
||||
- Used setup.cfg for development tools.
|
||||
- Removed code for documentation and for Windows portable version with GUI. Used new repositories for this. Cleaned up main repository.
|
||||
- Updated all dependencies.
|
||||
|
||||
**Translations:**
|
||||
- Updated DE, FR, CS and PT translations.
|
||||
- Added ES translations.
|
||||
|
||||
|
||||
## Version 1.7 (2015-02-16)
|
||||
|
||||
https://github.com/OpenSlides/OpenSlides/milestones/1.7
|
||||
|
||||
**Core:**
|
||||
- New feature to tag motions, agenda and assignments.
|
||||
- Fixed search index problem to index contents of many-to-many tables (e. g. tags of a motion).
|
||||
- Fixed AttributeError in chatbox on_open method.
|
||||
|
||||
**Motions:**
|
||||
- New Feature to create amendments, which are related to a parent motion.
|
||||
- Added possibility to hide motions from non staff users in some states.
|
||||
|
||||
**Assignments:**
|
||||
- Fixed permissions to alter assignment polls.
|
||||
|
||||
**Other:**
|
||||
- Cleaned up utils.views to increase performance when fetching single objects from the database for a view (#1378).
|
||||
- Fixed bug on projector which was not updated when an object was deleted.
|
||||
- Fixed bug and show special characters in PDF like ampersand (#1415).
|
||||
- Updated pdf.js to 1.0.907.
|
||||
- Improve the usage of bsmselect jquery plugin.
|
||||
- Updated translations.
|
||||
|
||||
|
||||
## Version 1.6.1 (2014-12-08)
|
||||
|
||||
https://github.com/OpenSlides/OpenSlides/milestones/1.6.1
|
||||
|
||||
**Agenda:**
|
||||
- Fixed error in item numbers.
|
||||
|
||||
**Motions:**
|
||||
- Show supporters on motion slide if available.
|
||||
- Fixed motion detail view template. Added block to enable extra content via plugins.
|
||||
|
||||
**Assignments:**
|
||||
- Fixed PDF build error when an election has more than 20 posts or candidates.
|
||||
|
||||
**Participants:**
|
||||
- Fixed participant csv import with group ids:
|
||||
- Allowed to add multiple groups in csv group id field, e. g. "3,4".
|
||||
- Fixed bug that group ids greater than 9 can not be imported.
|
||||
- Updated error message if group id does not exists.
|
||||
|
||||
**Other:**
|
||||
- Fixed CKEditor stuff (added insertpre plugin and removed unused code).
|
||||
- Updated French, German and Czech translation.
|
||||
|
||||
|
||||
## Version 1.6 (2014-06-02)
|
||||
|
||||
https://github.com/OpenSlides/OpenSlides/milestones/1.6
|
||||
|
||||
**Dashboard:**
|
||||
- Added shortcuts for the countdown.
|
||||
- Enabled copy and paste in widgets.
|
||||
|
||||
**Agenda:**
|
||||
- New projector view with the current list of speakers.
|
||||
- Added CSV import of agenda items.
|
||||
- Added automatic numbering of agenda items.
|
||||
- Fixed organizational item structuring.
|
||||
|
||||
**Motions:**
|
||||
- New slide for vote results.
|
||||
- Created new categories during CSV import.
|
||||
|
||||
**Assignments/Elections:**
|
||||
- Coupled assignment candidates with list of speakers.
|
||||
- Created a poll description field for each assignment poll.
|
||||
- New slide for election results.
|
||||
|
||||
**Participants:**
|
||||
- Disabled dashboard widgets by default.
|
||||
- Added form field for multiple creation of new participants.
|
||||
|
||||
**Files:**
|
||||
- Enabled update and delete view for uploader refering to his own files.
|
||||
|
||||
**Other:**
|
||||
- Added global chatbox for managers.
|
||||
- New config option to set the 100 % base for polls (motions/elections).
|
||||
- Changed api for plugins. Used entry points to detect them automaticly. Load them automaticly from plugin directory of Windows portable version.
|
||||
- Added possibility to use custom templates and static files in user data path directory.
|
||||
- Changed widget api. Used new metaclass.
|
||||
- Changed api for main menu entries. Used new metaclass.
|
||||
- Inserted api for the personal info widget. Used new metaclass.
|
||||
- Renamed config api classes. Changed permission system for config pages.
|
||||
- Regrouped config collections and pages.
|
||||
- Renamed some classes of the poll api.
|
||||
- Renamed method and attribute of openslides.utils.views.PermissionMixin.
|
||||
- Added api for absolute urls in models.
|
||||
- Inserted command line option to translate config strings during database setup.
|
||||
- Enhanced http error pages.
|
||||
- Improved responsive design for templates.
|
||||
- Fixed headings on custom slides without text.
|
||||
- Moved dashboard and select widgets view from projector to core app.
|
||||
- Renamed and cleaned up static direcories.
|
||||
- Used jsonfield as required package. Removed jsonfield code.
|
||||
- Added new package backports.ssl_match_hostname for portable build script.
|
||||
- Used new app "django-ckeditor-updated" to render WYSIWYG html editors. Removed CKEditor from sources.
|
||||
- Only reload the webserver in debug-mode.
|
||||
|
||||
|
||||
## Version 1.5.1 (2014-03-31)
|
||||
|
||||
https://github.com/OpenSlides/OpenSlides/milestones/1.5.1
|
||||
|
||||
**Projector:**
|
||||
- Fixed path and config help text for logo on the projector.
|
||||
|
||||
**Agenda:**
|
||||
- Fixed permission error in the list of speakers widget.
|
||||
- Fixed Item instance method is_active_slide().
|
||||
|
||||
**Motion:**
|
||||
- Fixed sorting of motions concerning the identifier. Used natsort and DataTables Natural Sort Plugin.
|
||||
|
||||
**Participant:**
|
||||
- Added permission to see participants to the manager group.
|
||||
- Fixed user status view for use without Javascript.
|
||||
|
||||
**Files:**
|
||||
- Fixed error when an uploaded file was removed from filesystem.
|
||||
|
||||
**Other:**
|
||||
- Set minimum Python version to 2.6.9. Fixed setup file for use with Python 2.6.
|
||||
- Used unicode font for circle in ballot pdf. Removed Pillow dependency package.
|
||||
- Fixed http status code when requesting a non-existing static page using Tornado web server.
|
||||
- Fixed error in main script when using other database engine.
|
||||
- Fixed error on motion PDF with nested lists.
|
||||
|
||||
|
||||
## Version 1.5 (2013-11-25)
|
||||
|
||||
https://github.com/OpenSlides/OpenSlides/milestones/1.5
|
||||
|
||||
**Projector:**
|
||||
- New feature: Show PDF presentations on projector (with included pdf.js).
|
||||
- Improved projector update process via new websocket API (using sockjs and tornado).
|
||||
- New projector template with twitter bootstrap.
|
||||
- Improved projector zoom and scroll behaviour.
|
||||
|
||||
**Agenda:**
|
||||
- New config option: couple countdown with list of speakers.
|
||||
- Used HTML editor (CKEditor) for agenda item text field.
|
||||
- Added additional input format for agenda item duration field.
|
||||
|
||||
**Motions:**
|
||||
- Enabled attachments for motions.
|
||||
- Refactored warnings on CSV import view.
|
||||
|
||||
**Elections:**
|
||||
- Refactored assignment app to use class based views instead of functions.
|
||||
|
||||
**Polls:**
|
||||
- Added percent base to votes cast values.
|
||||
|
||||
**Participants:**
|
||||
- Updated access data PDF: WLAN access (with QRCode for WLAN ssid/password) and OpenSlides access (with QRCode for system URL), printed on a single A4 page for each participant.
|
||||
|
||||
**Other:**
|
||||
- Full text search integration (with Haystack and Whoosh).
|
||||
- New start script with new command line options (see python manage.py --help)
|
||||
- Fixed keyerror on user settings view.
|
||||
- New messages on success or error of many actions like creating or editing objects.
|
||||
- Changed messages backend, used Django's default now.
|
||||
- A lot of template fixes and improvements.
|
||||
- Extended css style options in CKEditor.
|
||||
- Added feature to config app to return the default value for a key.
|
||||
- Cleaned up OpenSlides utils views.
|
||||
- Improved README (now with install instructions and used components).
|
||||
- Updated all required package versions.
|
||||
- Used flake8 instead of pep8 for style check, sort all import statements with isort.
|
||||
- Added Portuguese translation (Thanks to Marco A. G. Pinto).
|
||||
- Switched to more flexible versions of required third party packages.
|
||||
- Updated to Django 1.6.x.
|
||||
- Updated German documentation.
|
||||
- Change license from GPLv2+ to MIT, see LICENSE file.
|
||||
|
||||
|
||||
## Version 1.4.2 (2013-09-10)
|
||||
|
||||
https://github.com/OpenSlides/OpenSlides/milestones/1.4.2
|
||||
|
||||
- Used jQuery plugin bsmSelect for better ``<select multiple>`` form elements.
|
||||
- New config option to disable paragraph numbering in motion pdf. (Default value: disabled.)
|
||||
- Removed max value limitation in config field 'motion_min_supporters'.
|
||||
- Removed supporters signature field in motion pdf.
|
||||
- Fixed missing creation time of motion version. Show now string if identifier is not set (in widgets and motion detail).
|
||||
- Fixed error when a person is deleted.
|
||||
- Fixed deleting of assignments with related agenda items.
|
||||
- Fixed wrong ordering of agenda items after order change.
|
||||
- Fixed error in portable version: Open browser on localhost when server listens to 0.0.0.0.
|
||||
- Fixed typo and updated translations.
|
||||
- Updated CKEditor from 4.1.1 to 4.2. Fixed errors in MS Internet Explorer.
|
||||
- Updated to Django 1.5.2.
|
||||
|
||||
|
||||
## Version 1.4.1 (2013-07-29)
|
||||
|
||||
https://github.com/OpenSlides/OpenSlides/milestones/1.4.1
|
||||
|
||||
- Fixed tooltip which shows the end of each agenda item.
|
||||
- Fixed duration of agenda with closed agenda items.
|
||||
- Disabled deleting active version of a motion.
|
||||
- Start browser on custom IP address.
|
||||
- Fixed wrong URLs to polls in motion detail view.
|
||||
- Added Czech translation.
|
||||
|
||||
|
||||
## Version 1.4 (2013-07-10)
|
||||
|
||||
https://github.com/OpenSlides/OpenSlides/milestones/1.4
|
||||
|
||||
**Agenda:**
|
||||
- New feature: list of speakers for each agenda item which saves begin and end time of each speaker; added new widget and overlay on the dashboard for easy managing and presenting lists of speakers.
|
||||
- New item type: organisational item (vs. agenda item).
|
||||
- New duration field for each item (with total time calculation for end time of event).
|
||||
- Better drag'n'drop sorting of agenda items (with nestedSortable jQuery plugin).
|
||||
|
||||
**Motions:**
|
||||
- Integrated CKEditor to use allowed HTML formatting in motion text/reason. With server-side whitelist filtering of HTML tags (with bleach) and HTML support for reportlab in motion pdf.
|
||||
- New motion API.
|
||||
- Support for serveral submitters.
|
||||
- New workflow concept with two built-in workflows:
|
||||
1) complex workflow (like in OpenSlides <= v1.3)
|
||||
2) simple workflow (only 4 states: submitted -> acceptednot decided; no versioning)
|
||||
- Categories for grouping motions.
|
||||
- New modifiable identifier.
|
||||
- New motion version diff view. Improved history table in motion detail view.
|
||||
- New config variable 'Stop submitting of new motions' (for non-manager users).
|
||||
- Updated motion status log.
|
||||
- Updated csv import.
|
||||
|
||||
**Participants:**
|
||||
- New feature: qr-code for system url on participants password pdf.
|
||||
- Update default groups and permissions.
|
||||
- New participant field: 'title'.
|
||||
- Removed participants field 'type'. Use 'group' field instead. Updated csv import.
|
||||
- Added warning if non-superuser removes his last group containing permission to manage participants.
|
||||
|
||||
**Other:**
|
||||
- New html template based on twitter bootstrap.
|
||||
- New GUI frontend for the Windows portable version.
|
||||
- New command to backup sqlite database.
|
||||
- New mediafile app (files) to upload/download files via frontend.
|
||||
- Used Tornado web server (instead of Django's default development server).
|
||||
- Updated win32 portable version to use Tornado.
|
||||
- Integrated DataTables jQuery plugin for overview tables of motions, elections and participants (for client side sorting/filtering/pagination).
|
||||
- New overlay API for projector view.
|
||||
- New config app: Apps have to define config vars only once; config pages and forms are created automatically.
|
||||
- Moved version page out of the config app.
|
||||
- Changed version number api for plugins.
|
||||
- Moved widget with personal info to account app. Inserted info about lists of speakers.
|
||||
- Updated to Django 1.5.
|
||||
- Dropped support for python 2.5.
|
||||
- Updated packaging (setup.py and portable).
|
||||
- Open all PDFs in a new tab.
|
||||
- Changed Doctype to HTML5.
|
||||
- Updated German documentation (especially sections about agenda and motions).
|
||||
- Several minor fixes and improvements.
|
||||
|
||||
|
||||
## Version 1.3.1 (2013-01-09)
|
||||
|
||||
https://github.com/OpenSlides/OpenSlides/milestones/1.3.1
|
||||
|
||||
- Fixed unwanted automatical language switching on projector view if more than one browser languages send projector request to OpenSlides (#434)
|
||||
|
||||
|
||||
## Version 1.3 (2012-12-10)
|
||||
|
||||
https://github.com/OpenSlides/OpenSlides/milestones/1.3
|
||||
|
||||
**Projector:**
|
||||
- New public dashboard which allows access for all users per default. (#361) (changed from the old, limited projector control page)
|
||||
- New dashboard widgets:
|
||||
- welcome widget (shows static welcome title and text)
|
||||
- participant widget
|
||||
- group widget
|
||||
- personal widget (shows my motions and my elections)
|
||||
- Hide scrollbar in projector view.
|
||||
- Added cache for AJAX version of the projector view.
|
||||
- Moved projector control icons into projector live widget. (#403)
|
||||
- New weight field for custom slides (to order custom slides in widget).
|
||||
- Fixed drag'n'drop behaviour of widgets into empty dashboard column.
|
||||
- Fixed permissions for agenda, motion and assignment widgets (set to projector.can_manage_projector).
|
||||
|
||||
**Agenda:**
|
||||
- Fixed slide error if agenda item deleted. (#330)
|
||||
|
||||
**Motions:**
|
||||
- Translation: Changed 'application' to 'motion'.
|
||||
- Fixed: Manager could not edit supporters. (#336)
|
||||
- Fixed attribute error for anonymous users in motion view. (#329)
|
||||
- Set default sorting of motions by number (in widget).
|
||||
- CSV import allows to import group as submitter. (#419)
|
||||
- Updated motion code for new user API.
|
||||
- Rewrote motion views as class based views.
|
||||
|
||||
**Elections:**
|
||||
- User can block himself/herself from candidate list after delete his/her candidature.
|
||||
- Show blocked candidates in separate list.
|
||||
- Mark elected candidates in candidate list. (#374)
|
||||
- Show linebreaks in description. (#392)
|
||||
- Set default sorting of elections by name (in widget).
|
||||
- Fixed redirect from a poll which does not exists anymore.
|
||||
- Changed default permissions of anonymous user to see elections. (#334)
|
||||
- Updated assignment code for new user API.
|
||||
|
||||
**Participants:**
|
||||
- New user and group API.
|
||||
- New group option to handle a group as participant (and thus e.g. as submitter of motion).
|
||||
- CSV import does not delete existing users anymore and append users as new users.
|
||||
- New user field 'about me'. (#390)
|
||||
- New config option for sorting users by first or last name (in participant lists, elections and motions). (#303)
|
||||
- Allowed whitespaces in username, default: ``<firstname lastname>`` (#326)
|
||||
- New user and group slides. (#176)
|
||||
- Don't allow to deactivate the administrator or themself.
|
||||
- Don't allow to delete themself.
|
||||
- Renamed participant field 'groups' to 'structure level' (German: Gliederungsebene).
|
||||
- Rewrote participant views as class based views.
|
||||
- Made OpenSlides user a child model of Django user model.
|
||||
- Appended tests.
|
||||
- Fixed error to allow admins to delete anonymous group
|
||||
|
||||
**Other:**
|
||||
- Added French translation (Thanks to Moira).
|
||||
- Updated setup.py to make an openslides python package.
|
||||
- Removed frontpage (welcome widget contains it's content) and redirect '/' to dashboard url.
|
||||
- Added LOCALE_PATHS to openslides_settings to avoid deprecation in Django 1.5.
|
||||
- Redesigned the DeleteView (append QuestionMixin to send question via the django message API).
|
||||
- Fixed encoding error in settings.py. (#349)
|
||||
- Renamed openslides_settings.py to openslides_global_settings.py.
|
||||
- New default path to database file (XDG_DATA_HOME, e.g. ~/.local/share/openslides/).
|
||||
- New default path to settings file (XDG_CONFIG_HOME, e.g. ~/.config/openslides/).
|
||||
- Added special handling to determine location of database and settings file in portable version.
|
||||
- Don't use similar characters in generated passwords (no 'Il10oO').
|
||||
- Localised the datetime in PDF header. (#296)
|
||||
- Used specific session cookie name. (#332)
|
||||
- Moved code repository from hg to git (incl. some required updates, e.g. version string function).
|
||||
- Updated German translations.
|
||||
- Several code optimizations.
|
||||
- Several minor and medium issues and errors were fixed.
|
||||
|
||||
|
||||
## Version 1.2 (2012-07-25)
|
||||
|
||||
https://github.com/OpenSlides/OpenSlides/milestones/1.2
|
||||
|
||||
**General:**
|
||||
- New welcome page with customizable title and text.
|
||||
- OpenSlides portable win32 binary distribution.
|
||||
- New start script (start.py) to automatically create the default settings and the database, start the server and the default browser.
|
||||
- Add plugin system. Allow other django-apps to interact with OpenSlides.
|
||||
|
||||
**Projector:**
|
||||
- New projector dashboard to control all slides on projector.
|
||||
- New projector live view on projector dashboard.
|
||||
- Countdown calculation works now on server-side.
|
||||
- New Overlay messages to show additional information on a second projector layer.
|
||||
- Add custom slides.
|
||||
- Add a welcome slide.
|
||||
- Project application and assignment slides without an agenda item.
|
||||
- Update the projector once per second (only).
|
||||
|
||||
**Agenda:**
|
||||
- Add new comment field for agenda items.
|
||||
|
||||
**Elections (Assignments):**
|
||||
- New config option to publish voting results for selected winners only.
|
||||
|
||||
**Applications:**
|
||||
- Now, it's possible to deactivate the whole supporter system.
|
||||
- New import option: set status of all imported applications to 'permit'.
|
||||
- More log entries for all application actions.
|
||||
|
||||
**Participant:**
|
||||
- Add new comment field for participants.
|
||||
- Show translated permissions strings in user rols form.
|
||||
- Admin is redirect to 'change password' page.
|
||||
- New default user name: "firstname lastname".
|
||||
|
||||
**Other:**
|
||||
- Use Django's class based views.
|
||||
- Update to Django 1.4. Drop python 2.4 support for this reason.
|
||||
- Separate the code for the projector.
|
||||
- Rewrite the vote results table.
|
||||
- Rewrite the poll API.
|
||||
- Rewrite the config API. (Now any data which are JSON serializable can be stored.)
|
||||
- Improved CSV import for application and participants.
|
||||
- GUI improvements of web interface (e.g. sub navigations, overview tables).
|
||||
- Several minor and medium issues and errors were fixed.
|
||||
|
||||
|
||||
## Version 1.1 (2011-11-15)
|
||||
|
||||
https://github.com/OpenSlides/OpenSlides/milestones/1.1
|
||||
|
||||
**Agenda:**
|
||||
- [Feature] Agenda overview: New item-done-column for all non-manager (#7)
|
||||
- [Feature] Allow HTML-Tags in agenda item of text (#12)
|
||||
- [Feature] Split up hidden agenda items in new agenda table section (#13)
|
||||
|
||||
**Projector:**
|
||||
- [Feature] Assignment projector view layout improvements (#9)
|
||||
- [Bugfix] Zoom problem for sidebar div in beamer view (#5)
|
||||
- [Bugfix] Blue 'candidate elected line' not visible in projector ajax view (#6)
|
||||
- [Bugfix] Assignment projector view: Show results for elected candidates only (#11)
|
||||
- [Bugfix] Missing beamer scaling (#2)
|
||||
- [Bugfix] Assigment projector view: Removed empty character for no results cell. (#10)
|
||||
|
||||
**Applications:**
|
||||
- [Feature] Import applications (#55)
|
||||
- [Feature] Support trivial changes to an application (#56)
|
||||
- [Bugfix] Order submitter and supporter form fields by full name (#53)
|
||||
- [Bugfix] Application: Show profile instead of submitter username (#15)
|
||||
- [Bugfix] "Application: Only check enough supports in status ""pub""" (#16)
|
||||
|
||||
**Elections:**
|
||||
- [Feature] New button to show agenda item of selected application/assignment (#54)
|
||||
- [Feature] Open add-user-url in new tab. (#32)
|
||||
|
||||
**Applications/Elections:**
|
||||
- [Feature] Show voting results in percent (#48)
|
||||
|
||||
**Participants:**
|
||||
- [Feature] Filter displayed permissions in group editor (#59)
|
||||
- [Feature] Generate password after user creation automatically (#58)
|
||||
- [Bugfix] Encoding error (#1)
|
||||
- [Bugfix] List of participants (pdf) link not visible for users with see-particiants-permissions (#3)
|
||||
- [Bugfix] Use user.profile.get_type_display() instead of user.profile.type (#4)
|
||||
|
||||
**PDF:**
|
||||
- [Feature] Mark elected candidates in PDF (#31)
|
||||
- [Feature] New config option to set title and preamble text for application and assignment pdf (#33)
|
||||
- [Feature] New config option to set number of ballots in PDF (#26)
|
||||
- [Bugfix] Assignment ballot pdf: Wrong line break in group name with brackets (#8)
|
||||
- [Bugfix] Print available candidates in assignment pdf (#14)
|
||||
- [Bugfix] Show "undocumented" for result "-2" in application and assignment pdf (#17)
|
||||
|
||||
**Other:**
|
||||
- [Feature] Rights for anonymous (#45)
|
||||
- [Feature] Show counter for limited speaking time (#52)
|
||||
- [Feature] Reorderd config tab subpages (#61)
|
||||
- [Localize] i18n German: Use gender-specific strings (#51)
|
||||
- [Bugfix] ``<button>`` inside ``<a>`` tag not working in IE (#57)
|
||||
- [Bugfix] Change default sort for tables of applications, assignments, participants (#27)
|
||||
|
||||
|
||||
## Version 1.0 (2011-09-12)
|
||||
|
||||
https://github.com/OpenSlides/OpenSlides/tree/1.0/
|
File diff suppressed because it is too large
Load Diff
159
DEVELOPMENT.md
159
DEVELOPMENT.md
|
@ -1,159 +0,0 @@
|
|||
# Development of OpenSlides 4
|
||||
|
||||
## Requirements
|
||||
|
||||
You need git, bash, docker, docker-compose, make and openssl installed.
|
||||
|
||||
Go is needed to install https://github.com/FiloSottile/mkcert (but Go is not a requirement to start the development server). The development setup uses HTTPS per default. OpenSlides does not work with HTTP anymore since features are required (like http2) that only works in a secure environment.
|
||||
|
||||
## Before starting the development
|
||||
|
||||
Clone this repository:
|
||||
|
||||
$ git clone --recurse-submodules git@github.com:OpenSlides/OpenSlides.git
|
||||
|
||||
After cloning you need to initialize all submodules:
|
||||
|
||||
$ git submodule update --init
|
||||
|
||||
Finally, start the development server:
|
||||
|
||||
$ make run-dev
|
||||
|
||||
(This command won't run without sudo, or without having set up Docker to run without sudo - see their documentation)
|
||||
|
||||
You can access the services independently using their corresponding ports
|
||||
or access the full stack on
|
||||
|
||||
$ https://localhost:8000
|
||||
|
||||
## Running tests
|
||||
|
||||
To run all tests of all services, execute `run-service-tests`. TODO: Systemtests in this repo.
|
||||
|
||||
## Adding a new Service
|
||||
|
||||
$ git submodule add <git@myrepo.git>
|
||||
|
||||
Append `branch = main` to the new entry in the `.gitmodules` file. Verify,
|
||||
that it is there (the folder should have 160000 permissions: Submodule) with the
|
||||
current commit:
|
||||
|
||||
$ git diff --cached
|
||||
|
||||
Then, commit changes and create a pull request.
|
||||
|
||||
## Work in submodules
|
||||
|
||||
Create your own fork at github.
|
||||
|
||||
Remove the upstream repo as the origin in the submodule:
|
||||
|
||||
$ cd <submodule>
|
||||
$ git remote remove origin
|
||||
|
||||
Add your fork and the main repo as origin and upstream
|
||||
|
||||
$ git remote add origin `<your fork>`
|
||||
$ git remote add upstream `<main repo>`
|
||||
$ git fetch --all
|
||||
$ git checkout origin main
|
||||
|
||||
You can verify that your setup is correct using
|
||||
|
||||
$ git remote -v
|
||||
|
||||
The output should be similar to
|
||||
|
||||
origin git@github.com:<GithubUsername>/OpenSlides.git (fetch)
|
||||
origin git@github.com:<GithubUsername>/OpenSlides.git (push)
|
||||
upstream git@github.com:OpenSlides/OpenSlides.git (fetch)
|
||||
upstream git@github.com:OpenSlides/OpenSlides.git (push)
|
||||
|
||||
## Requirements for services
|
||||
|
||||
### Environment variables
|
||||
|
||||
These environment variables are available:
|
||||
|
||||
- `<SERVICE>_HOST`: The host from a required service
|
||||
- `<SERVICE>_PORT`: The port from a required service
|
||||
|
||||
Required services can be `MESSAGE_BUS`, `DATASTORE_WRITER`, `PERMISSION`, `AUTOUPDATE`,
|
||||
etc. For private services (e.g. a database dedicated to exactly one service),
|
||||
use the following syntax: `<SERVICE>_<PRIV_SERVICE>_<ATTRIBUTE>`, e.g. the
|
||||
Postgresql user for the datastore: `DATASTORE_POSTGRESQL_USER`.
|
||||
|
||||
### Makefile
|
||||
|
||||
A makefile must be provided at the root-level of the service. The currently
|
||||
required (phony) targets are:
|
||||
|
||||
- `run-tests`: Execute all tests from the submodule
|
||||
- `build-dev`: Build an image with the tag `openslides-<service>-dev`
|
||||
|
||||
### Build arguments in the Dockerfile
|
||||
|
||||
These build arguments should be supported by every service:
|
||||
|
||||
- `REPOSITORY_URL`: The git-url for the repository to use
|
||||
- `GIT_CHECKOUT`: A branch/tag/commit to check out during the build
|
||||
|
||||
Note that meaningful defaults should be provided in the Dockerfile.
|
||||
|
||||
## Developing on a single service
|
||||
|
||||
Go to the serivce and create a new branch (from main):
|
||||
|
||||
$ cd my-service
|
||||
$ git status # -> on main?
|
||||
$ git checkout -b my-feature
|
||||
|
||||
Run OpenSlides in development mode (e.g. in a new terminal):
|
||||
|
||||
$ make run-dev
|
||||
|
||||
After making some changes in my-service, create a commit and push to your fork
|
||||
|
||||
$ git add -A
|
||||
$ git commit -m "A meaningful commit message here"
|
||||
$ git push origin -u my-feature
|
||||
|
||||
As the last step, you can create a PR on Github. After merging, these steps are
|
||||
required to be executed in the main repo:
|
||||
|
||||
$ cd my-service
|
||||
$ git pull upstream main
|
||||
$ cd ..
|
||||
$ git diff # -> commit hash changed for my-service
|
||||
|
||||
If the update commit should be a PR:
|
||||
|
||||
$ git checkout -b updated-my-service
|
||||
$ git commit -am "Updated my-service"
|
||||
$ git push origin updated-my-service
|
||||
|
||||
Or a direct push on main:
|
||||
|
||||
$ git commit -am "Updated my-service"
|
||||
$ git push origin main
|
||||
|
||||
## Working with Submodules
|
||||
|
||||
After working in many services with different branches, this command checks
|
||||
out `main` (or the given branch in the .gitmodules) in all submodules and
|
||||
pulls main from upstream (This requres to have `upstream`set up as a remote
|
||||
in all submodules):
|
||||
|
||||
$ git submodule foreach -q --recursive 'git checkout $(git config -f $toplevel/.gitmodules submodule.$name.branch || echo main); git pull upstream $(git config -f $toplevel/.gitmodules submodule.$name.branch || echo main)'
|
||||
|
||||
This command has can also be called from the makefile using:
|
||||
|
||||
$ make services-to-main
|
||||
|
||||
When changing the branch in the main repo (this one), the submodules do not
|
||||
automatically gets changed. THis ocmmand checks out all submodules to the given
|
||||
commits in the main repo:
|
||||
|
||||
$ git submodule update
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
========================
|
||||
OpenSlides Development
|
||||
========================
|
||||
|
||||
Check requirements
|
||||
''''''''''''''''''
|
||||
|
||||
- ``docker``
|
||||
- ``docker-compose``
|
||||
- ``git``
|
||||
- ``make``
|
||||
|
||||
**Note about migrating from development setups before version 3.4**: You must set the
|
||||
``OPENSLIDES_USER_DATA_DIR`` variable in your ``server/personal_data/var/settings.py``
|
||||
to ``'/app/personal_data/var'``. Another way is to just delete this file. It is
|
||||
recreated with the right paths afterwards.
|
||||
|
||||
|
||||
Get OpenSlides source code
|
||||
''''''''''''''''''''''''''
|
||||
|
||||
Clone current master version from `OpenSlides GitHub repository
|
||||
<https://github.com/OpenSlides/OpenSlides/>`_::
|
||||
|
||||
git clone https://github.com/OpenSlides/OpenSlides.git --recurse-submodules
|
||||
cd OpenSlides
|
||||
|
||||
When updating the repository, submodules must be updated explicitly, too::
|
||||
|
||||
git submodule update
|
||||
|
||||
Start the development setup
|
||||
'''''''''''''''''''''''''''
|
||||
|
||||
Use `make` to start the setup::
|
||||
|
||||
make run-dev
|
||||
|
||||
All your data (database, config, mediafiles) is stored in ``server/personal_data/var``.
|
||||
To stop the setup press Ctrl+C. To clean up the docker containers run::
|
||||
|
||||
make stop-dev
|
||||
|
||||
Running the test cases
|
||||
''''''''''''''''''''''
|
||||
|
||||
For all services in submodules check out the documentation there.
|
||||
|
||||
|
||||
Server tests and scripts
|
||||
------------------------
|
||||
|
||||
You need to have python (>=3.8) and python-venv installed. Change your workdirectory to the server::
|
||||
|
||||
cd server
|
||||
|
||||
Setup an python virtual environment. If you have already done it, you can skip this step::
|
||||
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -U -r requirements.txt
|
||||
|
||||
Make sure you are using the correct python version (e.g. try with explicit minor version: ``python3.8``). Activate it::
|
||||
|
||||
source .venv/bin/activate
|
||||
|
||||
To deactivate it type ``deactivate``. Running all tests and linters::
|
||||
|
||||
black openslides/ tests/
|
||||
flake8 openslides/ tests/
|
||||
mypy openslides/ tests/
|
||||
isort -rc openslides/ tests/
|
||||
pytest tests/
|
||||
|
||||
Client tests
|
||||
------------
|
||||
|
||||
You need `node` and `npm` installed. Change to the client's directory. For the first time, install all dependencies::
|
||||
|
||||
cd client/
|
||||
npm install
|
||||
|
||||
Run client tests::
|
||||
|
||||
npm test
|
||||
|
||||
Fix the code format and lint it with::
|
||||
|
||||
npm run cleanup
|
||||
|
||||
To extract translations run::
|
||||
|
||||
npm run extract
|
4
LICENSE
4
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Since 2011 Authors of OpenSlides, see AUTHORS
|
||||
Copyright (c) 2011-2021 Authors of OpenSlides, see AUTHORS
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
55
Makefile
55
Makefile
|
@ -1,52 +1,23 @@
|
|||
run-integration-tests:
|
||||
@echo "Start OpenSlides Dev"
|
||||
make run-dev ARGS="-d"
|
||||
@echo "Start integration tests"
|
||||
make cypress-docker
|
||||
docker-compose -f integration/docker-compose.yml up
|
||||
@echo "Stop OpenSlides Dev"
|
||||
make stop-dev
|
||||
|
||||
run-service-tests:
|
||||
git submodule foreach 'make run-tests'
|
||||
|
||||
build-dev:
|
||||
./dev-commands/submodules-do.sh 'make build-dev'
|
||||
make -C proxy build-dev
|
||||
make -C caddy build-dev
|
||||
git submodule foreach 'make build-dev'
|
||||
docker-compose -f docker/docker-compose.dev.yml build
|
||||
|
||||
run-dev: | build-dev
|
||||
docker-compose -f docker/docker-compose.dev.yml up $(ARGS)
|
||||
|
||||
run-dev-otel: | build-dev
|
||||
docker-compose -f docker/docker-compose.dev.yml -f docker/dc.otel.dev.yml up $(ARGS)
|
||||
USER_ID=$$(id -u $${USER}) GROUP_ID=$$(id -g $${USER}) docker-compose -f docker/docker-compose.dev.yml up
|
||||
|
||||
stop-dev:
|
||||
docker-compose -f docker/docker-compose.dev.yml down --volumes --remove-orphans
|
||||
docker-compose -f docker/docker-compose.dev.yml down
|
||||
|
||||
stop-dev-otel:
|
||||
docker-compose -f docker/docker-compose.dev.yml -f docker/dc.otel.dev.yml down --volumes --remove-orphans
|
||||
|
||||
copy-node-modules:
|
||||
docker-compose -f docker/docker-compose.dev.yml exec client bash -c "cp -r /app/node_modules/ /app/src/"
|
||||
mv openslides-client/client/src/node_modules/ openslides-client/client/
|
||||
server-shell:
|
||||
docker-compose -f docker/docker-compose.dev.yml run --entrypoint="" server docker/wait-for-dev-dependencies.sh
|
||||
USER_ID=$$(id -u $${USER}) GROUP_ID=$$(id -g $${USER}) docker-compose -f docker/docker-compose.dev.yml run --entrypoint="" server bash
|
||||
docker-compose -f docker/docker-compose.dev.yml down
|
||||
|
||||
reload-proxy:
|
||||
docker-compose -f docker/docker-compose.dev.yml exec -w /etc/caddy proxy caddy reload
|
||||
|
||||
services-to-main:
|
||||
./services-to-main.sh
|
||||
|
||||
submodules-origin-to-upstream:
|
||||
# You may only use this one time after cloning this repository.
|
||||
# Will set the upstream remote to "origin"
|
||||
git submodule foreach -q --recursive 'git remote rename origin upstream'
|
||||
|
||||
cypress-open:
|
||||
cd integration; npm run cypress:open
|
||||
|
||||
cypress-run:
|
||||
cd integration; npm run cypress:run
|
||||
|
||||
cypress-docker:
|
||||
docker-compose -f integration/docker-compose.yml build
|
||||
docker-compose -f integration/docker-compose.yml up
|
||||
clear-cache:
|
||||
docker-compose -f docker/docker-compose.dev.yml exec redis redis-cli flushall
|
||||
docker-compose -f docker/docker-compose.dev.yml restart autoupdate
|
||||
docker-compose -f docker/docker-compose.dev.yml restart server
|
||||
|
|
57
README.md
57
README.md
|
@ -1,57 +0,0 @@
|
|||
# OpenSlides
|
||||
|
||||
## What is OpenSlides?
|
||||
|
||||
OpenSlides is a free, web based presentation and assembly system for
|
||||
managing and projecting agenda, motions and elections of an assembly. See
|
||||
https://openslides.com for more information.
|
||||
|
||||
|
||||
## Using OpenSlides productively
|
||||
|
||||
__OpenSlides 4 (this) is currently in beta version!__
|
||||
|
||||
If you are just looking to use OpenSlides in a productive manner, please refer
|
||||
to the [OpenSlides 3.4 (stable)](https://github.com/OpenSlides/OpenSlides/tree/stable/3.4.x)
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
### Requirements
|
||||
|
||||
You need [Docker](https://docs.docker.com/engine/install/) and [Docker
|
||||
Compose](https://docs.docker.com/compose/install/).
|
||||
|
||||
### Setup OpenSlides
|
||||
|
||||
For a productive setup of OpenSlides get the [OpenSlides manage
|
||||
tool](https://github.com/OpenSlides/openslides-manage-service/releases/tag/latest)
|
||||
from GitHub and make it executable. E. g. run:
|
||||
|
||||
$ wget https://github.com/OpenSlides/openslides-manage-service/releases/download/latest/openslides
|
||||
$ chmod +x openslides
|
||||
|
||||
Then follow the instructions outlined in the [OpenSlides Manage
|
||||
Service](https://github.com/OpenSlides/openslides-manage-service).
|
||||
|
||||
|
||||
## Development
|
||||
|
||||
For further information about developing OpenSlides, refer to [the development
|
||||
readme](DEVELOPMENT.md).
|
||||
|
||||
### Architecture of OpenSlides 4
|
||||
|
||||
![System architecture of OpenSlides 4](https://raw.githubusercontent.com/wiki/OpenSlides/OpenSlides/OS4/img/OpenSlides4.svg)
|
||||
|
||||
Read more about our [concept of OpenSlides 4.0](https://github.com/OpenSlides/OpenSlides/wiki/DE%3AKonzept-OpenSlides-4).
|
||||
|
||||
The technical documentation about the internals, requests and functionality can
|
||||
be found [in the wiki](https://github.com/OpenSlides/OpenSlides/wiki/DE%3AKonzept-OpenSlides-4).
|
||||
|
||||
|
||||
## License and authors
|
||||
|
||||
OpenSlides is Free/Libre Open Source Software (FLOSS), and distributed
|
||||
under the MIT License, see ``LICENSE`` file. The authors of OpenSlides are
|
||||
mentioned in the ``AUTHORS`` file.
|
|
@ -0,0 +1,159 @@
|
|||
============
|
||||
OpenSlides
|
||||
============
|
||||
|
||||
What is OpenSlides?
|
||||
===================
|
||||
|
||||
OpenSlides is a free, web-based presentation and assembly system for
|
||||
managing and projecting agenda, motions, and elections of assemblies. See
|
||||
https://openslides.com for more information.
|
||||
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
The main deployment method is using Git, Docker and Docker Compose. You only need
|
||||
to have these tools installed and no further dependencies (m4 may not come preinstalled on your system). If you want a simpler
|
||||
setup or are interested in developing, please refer to `development
|
||||
instructions <DEVELOPMENT.rst>`_.
|
||||
|
||||
Get OpenSlides
|
||||
--------------
|
||||
|
||||
First, you have to clone this repository to the latest stable branch::
|
||||
|
||||
git clone -b stable/3.4.x --single-branch https://github.com/OpenSlides/OpenSlides.git --recurse-submodules
|
||||
|
||||
**Note about migrating from version 3.3 or earlier**: With OpenSlides 3.4 submodules
|
||||
and a Docker setup were introduced. If you ran into problems try to delete your
|
||||
``settings.py``. If you have an old checkout you need to check out the current master
|
||||
first and initialize all submodules::
|
||||
|
||||
git submodule update --init
|
||||
|
||||
Setup Docker Compose
|
||||
--------------------
|
||||
|
||||
You need to build the Docker images and have to setup some configuration. First,
|
||||
configure HTTPS by checking the `Using HTTPS`_ section. In this section are
|
||||
reasons why HTTPS is required for large deployments.
|
||||
|
||||
Go to ``docker`` subdirectory::
|
||||
|
||||
cd docker
|
||||
|
||||
Then build all images with this script::
|
||||
|
||||
./build.sh all
|
||||
|
||||
You must define a Django secret key in ``secrets/django.env``, for example::
|
||||
|
||||
printf "DJANGO_SECRET_KEY='%s'\n" \
|
||||
"$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 64)" > secrets/django.env
|
||||
|
||||
We also strongly recommend that you set a secure admin password but it is not
|
||||
strictly required. If you do not set an admin password, the default login
|
||||
credentials will be displayed on the login page. Setting the admin password::
|
||||
|
||||
cp secrets/adminsecret.env.example secrets/adminsecret.env
|
||||
vi secrets/adminsecret.env
|
||||
|
||||
Afterwards, generate the configuration file::
|
||||
|
||||
m4 docker-compose.yml.m4 > docker-compose.yml
|
||||
|
||||
You can configure OpenSlides using the `.env` file. See `More settings`_. Another
|
||||
hint: If you choose to deploy the default configuration, a https certificate is
|
||||
needed, so make sure you have set it up beforehand.
|
||||
|
||||
Finally, you can start the instance using ``docker-compose``::
|
||||
|
||||
docker-compose up
|
||||
|
||||
OpenSlides is accessible on https://localhost/ (or https, if configured).
|
||||
|
||||
You can also use daemonized instance::
|
||||
|
||||
docker-compose up -d
|
||||
docker-compose logs
|
||||
docker-compose down
|
||||
|
||||
Using HTTPS
|
||||
-----------
|
||||
|
||||
The main reason (next to obviously security ones) HTTPS is required originates
|
||||
from the need of HTTP/2. OpenSlides uses streaming responses to asynchronously
|
||||
send data to the client. With HTTP/1.1 one TCP-Connection per request is opened.
|
||||
Browsers limit the amount of concurrent connections
|
||||
(`reference <https://docs.pushtechnology.com/cloud/latest/manual/html/designguide/solution/support/connection_limitations.html>`_),
|
||||
so you are limited in opening tabs. HTTPS/2 just uses one connection per browser
|
||||
and eliminates these restrictions. The main point to use HTTPS is that browsers
|
||||
only use HTTP/2 if HTTPS is enabled.
|
||||
|
||||
Setting up HTTPS
|
||||
""""""""""""""""
|
||||
|
||||
Use common providers for retrieving a certificate and private key for your
|
||||
deployment. Place the certificate and private key in ``caddy/certs/cert.pem``
|
||||
and ``caddy/certs/key.pem``. To use a self-signed localhost certificate, you can
|
||||
execute ``caddy/make-localhost-cert.sh``.
|
||||
|
||||
The certificate and key are put into the docker image into ``/certs/``, so
|
||||
setting up these files needs to be done before calling ``./build.sh``. When you
|
||||
update the files, you must run ``./build.sh proxy`` again. If you want to have a
|
||||
more flexible setup without the files in the image, you can also mount the
|
||||
folder or the certificate and key into the running containers if you wish to do
|
||||
so.
|
||||
|
||||
If both files are not present, OpenSlides will be configured to run with HTTP
|
||||
only. When mounting the files make sure, that they are present during the
|
||||
container startup.
|
||||
|
||||
Caddy, the proxy used, wants the user to persist the ``/data`` directory. If you
|
||||
are going to use HTTPS add a volume in your ``docker-compose.yml`` /
|
||||
``docker-stack.yml`` persisting the ``/data`` directory.
|
||||
|
||||
More settings
|
||||
-------------
|
||||
|
||||
When generating the ``docker-compose.yml``, more settings can be adjusted in the
|
||||
``docker/.env`` file. All changes for the backend are passed into djangos ``settings.py``.
|
||||
You can find more information about most settings in the `settings documentation
|
||||
<server/SETTINGS.rst>`_. To generate the ``docker-compose.yml`` use this command::
|
||||
|
||||
cd docker
|
||||
( set -a; source .env; m4 docker-compose.yml.m4 ) > docker-compose.yml
|
||||
|
||||
For an advanced database setup refer to the `advanced configuration
|
||||
<ADVANCED.rst>`_.
|
||||
|
||||
|
||||
Bugs, features and development
|
||||
================================
|
||||
|
||||
Feel free to open issues here on GitHub! Please use the right templates for
|
||||
bugs and features, and use them correctly. Pull requests are also welcome. For
|
||||
a general overview of the development setup refer the `development instructions
|
||||
<DEVELOPMENT.rst>`_.
|
||||
|
||||
For security relevant issues **do not** create public issues and refer to
|
||||
our `security policy <SECURITY.md>`_.
|
||||
|
||||
|
||||
Used software
|
||||
=============
|
||||
|
||||
OpenSlides uses the following projects or parts of them:
|
||||
|
||||
* several Python packages (see ``server/requirements/production.txt``)
|
||||
|
||||
* several JavaScript packages (see ``client/package.json``)
|
||||
|
||||
|
||||
License and authors
|
||||
===================
|
||||
|
||||
OpenSlides is Free/Libre Open Source Software (FLOSS), and distributed
|
||||
under the MIT License, see `LICENSE file <LICENSE>`_. The authors of OpenSlides are
|
||||
mentioned in the `AUTHORS file <AUTHORS>`_.
|
|
@ -0,0 +1,14 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Currently the recent versions (a year old at max) of the current major
|
||||
release are supported to get security fixes. For older versions we urge
|
||||
you to update to the most recent version of OpenSlides.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please let us know about the vulnerability by an email to
|
||||
[security@openslides.com](mailto:security@openslides.com). Do not create
|
||||
public issues for such issues. We will get in touch with you after reporting
|
||||
the issue.
|
|
@ -0,0 +1 @@
|
|||
Subproject commit fa74dfe888dfeb4d071279eeaff5bc221ecf85f8
|
|
@ -0,0 +1,20 @@
|
|||
import endpoint
|
||||
import invalid_host*
|
||||
|
||||
reverse_proxy /system/* autoupdate:8002 {
|
||||
flush_interval -1
|
||||
}
|
||||
|
||||
@server {
|
||||
path /apps/*
|
||||
path /rest/*
|
||||
path /server-version.txt
|
||||
}
|
||||
reverse_proxy @server server:8000
|
||||
|
||||
reverse_proxy /media/* media:8000
|
||||
|
||||
reverse_proxy client:4200
|
||||
}
|
||||
|
||||
import redirect*
|
|
@ -0,0 +1,15 @@
|
|||
localhost:8000 {
|
||||
reverse_proxy /system/* autoupdate:8002 {
|
||||
flush_interval -1
|
||||
}
|
||||
|
||||
@server {
|
||||
path /apps/*
|
||||
path /rest/*
|
||||
path /server-version.txt
|
||||
path /media/*
|
||||
}
|
||||
reverse_proxy @server server:8000
|
||||
|
||||
reverse_proxy client:4200
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
FROM caddy:2.3.0-alpine
|
||||
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
COPY entrypoint /entrypoint
|
||||
COPY certs /certs
|
||||
|
||||
ENTRYPOINT ["/entrypoint"]
|
||||
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
|
@ -0,0 +1,3 @@
|
|||
FROM caddy:2.3.0-alpine
|
||||
|
||||
COPY Caddyfile.dev /etc/caddy/Caddyfile
|
|
@ -0,0 +1,2 @@
|
|||
build-dev:
|
||||
docker build -t os3-proxy-dev -f Dockerfile.dev .
|
|
@ -0,0 +1,78 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
if [[ -z "$EXTERNAL_HTTP_PORT" ]] && [[ -z "$EXTERNAL_HTTPS_PORT" ]]; then
|
||||
echo "EXTERNAL_HTTP_PORT and EXTERNAL_HTTPS_PORT are not set. Aborting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f "/certs/key.pem" ]] && [[ -f "/certs/cert.pem" ]]; then
|
||||
certs_exists=1
|
||||
fi
|
||||
|
||||
if [[ -n "$EXTERNAL_HTTPS_PORT" ]] && [[ -z "$certs_exists" ]]; then
|
||||
echo "Configured https, but no certificates found. Aborting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# config: https
|
||||
if [[ -n "$EXTERNAL_HTTPS_PORT" ]] ; then
|
||||
cat <<EOF > /etc/caddy/endpoint
|
||||
https://:8001 {
|
||||
tls /certs/cert.pem /certs/key.pem
|
||||
EOF
|
||||
echo "Configured https"
|
||||
fi
|
||||
|
||||
# config: http and no https
|
||||
if [[ -n "$EXTERNAL_HTTP_PORT" ]] && [[ -z "$EXTERNAL_HTTPS_PORT" ]] ; then
|
||||
echo "http://:8000 {" > /etc/caddy/endpoint
|
||||
echo "Configured http only"
|
||||
fi
|
||||
|
||||
# config: https and additionally http -> create redirect-file
|
||||
if [[ -n "$EXTERNAL_HTTP_PORT" ]] && [[ -n "$EXTERNAL_HTTPS_PORT" ]] ; then
|
||||
cat <<EOF > /etc/caddy/redirect
|
||||
http://:8000 {
|
||||
redir https://$INSTANCE_DOMAIN{uri}
|
||||
}
|
||||
EOF
|
||||
echo "Configured http to https redirect"
|
||||
fi
|
||||
|
||||
# Add allowed hosts from $ALLOWED_HOSTS
|
||||
# If the variable is empty, all hosts are allowed.
|
||||
# The hosts are ORed, so the request is valid, if one host matches.
|
||||
# Example: ALLOWED_HOSTS="localhost:8000 127.0.0.1:8000"
|
||||
#
|
||||
# @invalid-host {
|
||||
# not {
|
||||
# header Host localhost:8000
|
||||
# header Host 127.0.0.1:8000
|
||||
# }
|
||||
# }
|
||||
# respond @invalid-host "Misdirected Request" 421 {
|
||||
# close
|
||||
# }
|
||||
if [[ ! -z "$ALLOWED_HOSTS" ]]; then
|
||||
cat <<EOF > /etc/caddy/invalid_host
|
||||
@invalid-host {
|
||||
not {
|
||||
EOF
|
||||
for host in $ALLOWED_HOSTS; do
|
||||
echo " host $host" >> /etc/caddy/invalid_host
|
||||
done
|
||||
cat <<EOF >> /etc/caddy/invalid_host
|
||||
}
|
||||
}
|
||||
respond @invalid-host "Misdirected Request" 421 {
|
||||
close
|
||||
}
|
||||
EOF
|
||||
echo "Configured allowed hosts: $ALLOWED_HOSTS"
|
||||
else
|
||||
echo "All hosts allowed"
|
||||
fi
|
||||
|
||||
exec "$@"
|
|
@ -4,8 +4,8 @@ set -e
|
|||
cd "$(dirname "$0")"
|
||||
|
||||
if [[ -f "certs/key.pem" ]] || [[ -f "certs/cert.pem" ]]; then
|
||||
echo "Certificate already exists."
|
||||
exit 0
|
||||
echo >&2 "Error: Certificate already exists."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! type 2>&1 >/dev/null openssl ; then
|
|
@ -0,0 +1,22 @@
|
|||
Owner = "OpenSlides"
|
||||
RepositoryName = "OpenSlides"
|
||||
|
||||
OutputType = "file"
|
||||
FileName = "CHANGELOG.md"
|
||||
|
||||
CurrentRef = "master"
|
||||
BaseBranch = "master"
|
||||
PreviousRef = "3.3"
|
||||
FutureCurrentRefName = "3.4"
|
||||
|
||||
ThresholdPreviousRef = 10
|
||||
ThresholdCurrentRef = 10
|
||||
|
||||
Debug = false
|
||||
DisplayLabel = false
|
||||
|
||||
# ignore everything labeld as "meta"
|
||||
LabelExcludes = ["meta"]
|
||||
LabelEnhancement = "feature"
|
||||
LabelDocumentation = "documentation"
|
||||
LabelBug = "bug"
|
|
@ -0,0 +1,7 @@
|
|||
last 1 Chrome version
|
||||
last 1 Firefox version
|
||||
last 2 Edge major versions
|
||||
last 2 Safari major versions
|
||||
last 2 iOS major versions
|
||||
Firefox ESR
|
||||
not IE 11
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "OpenSlides Client Documentation",
|
||||
"output": "../Compodoc/",
|
||||
"tsconfig": "./tsconfig.json",
|
||||
"disableCoverage": true,
|
||||
"hideGenerator": true,
|
||||
"theme": "material",
|
||||
"customLogo": "./src/assets/img/openslides-logo.svg",
|
||||
"customFavicon": "./src/assets/img/favicon.png"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
.git
|
||||
**/node_modules
|
||||
**/.vscode
|
||||
build.sh
|
|
@ -0,0 +1,13 @@
|
|||
# Editor configuration, see http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true,
|
||||
"useTabs": false,
|
||||
"bracketSpacing": true,
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"semi": true,
|
||||
"trailingComma": "none",
|
||||
"arrowParens": "avoid"
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
# OpenSlides 3 Client
|
||||
|
||||
### Documentation Info
|
||||
|
||||
The documentation can be generated by running `npm run compodoc`.
|
||||
A new web server will be started on http://localhost:8080
|
||||
Once running, the documentation will be updated automatically.
|
||||
|
||||
You can run it on another port, with adding your local port after the
|
||||
command. If no port specified, it will try to use 8080.
|
||||
|
||||
Please document new code using JSDoc tags.
|
||||
See https://compodoc.app/guides/jsdoc-tags.html for details.
|
||||
|
||||
### Translation
|
||||
|
||||
We are using ngx-translate for translation purposes.
|
||||
Use `npm run extract` to extract strings and update elements an with translation functions.
|
||||
|
||||
Language files can be found in `/src/assets/i18n`.
|
||||
|
||||
### Used software
|
||||
|
||||
OpenSlides uses the following software or parts of them:
|
||||
|
||||
- [@angular/animations@12.1.1](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/cdk-experimental@12.1.1](https://github.com/angular/components), License: MIT
|
||||
- [@angular/cdk@12.1.1](https://github.com/angular/components), License: MIT
|
||||
- [@angular/common@12.1.1](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/compiler@12.1.1](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/core@12.1.1](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/forms@12.1.1](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/material-moment-adapter@12.1.1](https://github.com/angular/components), License: MIT
|
||||
- [@angular/material@12.1.1](https://github.com/angular/components), License: MIT
|
||||
- [@angular/platform-browser-dynamic@12.1.1](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/platform-browser@12.1.1](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/router@12.1.1](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/service-worker@12.1.1](https://github.com/angular/angular), License: MIT
|
||||
- [@ngx-pwa/local-storage@12.0.0](https://github.com/cyrilletuzi/angular-async-local-storage), License: MIT
|
||||
- [@ngx-translate/core@13.0.0](https://github.com/ngx-translate/core), License: MIT
|
||||
- [@ngx-translate/http-loader@6.0.0](https://github.com/ngx-translate/http-loader), License: MIT
|
||||
- [@pebula/ngrid-material@4.0.0-alpha.3](undefined), License: UNKNOWN
|
||||
- [@pebula/ngrid@4.0.0-alpha.3](https://github.com/shlomiassaf/ngrid), License: MIT
|
||||
- [@pebula/utils@1.0.2](undefined), License: UNKNOWN
|
||||
- [@tinymce/tinymce-angular@4.2.4](https://github.com/tinymce/tinymce-angular), License: Apache-2.0
|
||||
- [@videojs/http-streaming@2.9.1](https://github.com/videojs/http-streaming), License: Apache-2.0
|
||||
- [acorn@8.4.1](https://github.com/acornjs/acorn), License: MIT
|
||||
- [chart.js@2.9.4](https://github.com/chartjs/Chart.js), License: MIT
|
||||
- [core-js@3.15.1](https://github.com/zloirock/core-js), License: MIT
|
||||
- [css-element-queries@1.2.3](https://github.com/marcj/css-element-queries), License: MIT
|
||||
- [exceljs@4.1.1](https://github.com/exceljs/exceljs), License: MIT
|
||||
- [file-saver@2.0.2](https://github.com/eligrey/FileSaver.js), License: MIT
|
||||
- [jszip@3.5.0](https://github.com/Stuk/jszip), License: (MIT OR GPL-3.0)
|
||||
- [lz4js@0.2.0](https://github.com/Benzinga/lz4js), License: ISC
|
||||
- [material-design-icons-iconfont@6.1.0](https://github.com/jossef/material-design-icons-iconfont), License: Apache-2.0
|
||||
- [moment@2.27.0](https://github.com/moment/moment), License: MIT
|
||||
- [ng-particles@2.6.0](https://github.com/matteobruni/tsparticles), License: MIT
|
||||
- [ng2-charts@2.4.0](https://github.com/valor-software/ng2-charts), License: ISC
|
||||
- [ng2-pdf-viewer@6.4.1](git+https://vadimdez@github.com/VadimDez/ng2-pdf-viewer), License: MIT
|
||||
- [ngx-device-detector@2.0.0](undefined), License: MIT*
|
||||
- [ngx-file-drop@11.1.0](https://github.com/georgipeltekov/ngx-file-drop), License: MIT
|
||||
- [ngx-mat-select-search@3.3.0](https://github.com/bithost-gmbh/ngx-mat-select-search), License: MIT
|
||||
- [ngx-material-timepicker@5.5.3](https://github.com/Agranom/ngx-material-timepicker), License: MIT
|
||||
- [ngx-papaparse@4.0.4](https://github.com/alberthaff/ngx-papaparse), License: MIT
|
||||
- [npm-license-crawler@0.2.1](http://github.com/mwittig/npm-license-crawler), License: BSD-3-Clause
|
||||
- [pdfjs-dist@2.5.207](https://github.com/mozilla/pdfjs-dist), License: Apache-2.0
|
||||
- [pdfmake@0.1.68](https://github.com/bpampuch/pdfmake), License: MIT
|
||||
- [po2json@1.0.0-beta-2](https://github.com/mikeedwards/po2json), License: LGPL-2.0-or-later
|
||||
- [rxjs@6.6.7](https://github.com/reactivex/rxjs), License: Apache-2.0
|
||||
- [tinymce@5.7.1](https://github.com/tinymce/tinymce-dist), License: LGPL-2.1
|
||||
- [tslib@2.3.0](https://github.com/Microsoft/tslib), License: 0BSD
|
||||
- [tsparticles@1.23.0](https://github.com/matteobruni/tsparticles), License: MIT
|
||||
- [video.js@7.13.3](https://github.com/videojs/video.js), License: Apache-2.0
|
||||
- [zone.js@0.11.4](https://github.com/angular/angular), License: MIT
|
||||
- [@angular-devkit/build-angular@12.1.1](https://github.com/angular/angular-cli), License: MIT
|
||||
- [@angular-devkit/schematics@12.1.1](https://github.com/angular/angular-cli), License: MIT
|
||||
- [@angular/cli@12.1.1](https://github.com/angular/angular-cli), License: MIT
|
||||
- [@angular/compiler-cli@12.1.1](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/language-service@12.1.1](https://github.com/angular/angular), License: MIT
|
||||
- [@biesbjerg/ngx-translate-extract-marker@1.0.0](https://github.com/biesbjerg/ngx-translate-extract-marker), License: MIT
|
||||
- [@biesbjerg/ngx-translate-extract@7.0.4](https://github.com/biesbjerg/ngx-translate-extract), License: MIT
|
||||
- [@compodoc/compodoc@1.0.9](https://github.com/compodoc/compodoc), License: MIT
|
||||
- [@schematics/angular@10.0.8](https://github.com/angular/angular-cli), License: MIT
|
||||
- [@types/jasmine@3.6.11](https://github.com/DefinitelyTyped/DefinitelyTyped), License: MIT
|
||||
- [@types/node@14.6.2](https://github.com/DefinitelyTyped/DefinitelyTyped), License: MIT
|
||||
- [@types/yargs@15.0.5](https://github.com/DefinitelyTyped/DefinitelyTyped), License: MIT
|
||||
- [codelyzer@6.0.0](https://github.com/mgechev/codelyzer), License: MIT
|
||||
- [husky@4.2.5](https://github.com/typicode/husky), License: MIT
|
||||
- [jasmine-core@3.8.0](https://github.com/jasmine/jasmine), License: MIT
|
||||
- [karma-chrome-launcher@3.1.0](https://github.com/karma-runner/karma-chrome-launcher), License: MIT
|
||||
- [karma-coverage@2.0.3](https://github.com/karma-runner/karma-coverage), License: MIT
|
||||
- [karma-jasmine-html-reporter@1.5.4](https://github.com/dfederm/karma-jasmine-html-reporter), License: MIT
|
||||
- [karma-jasmine@4.0.1](https://github.com/karma-runner/karma-jasmine), License: MIT
|
||||
- [karma@6.3.4](https://github.com/karma-runner/karma), License: MIT
|
||||
- [prettier@2.3.2](https://github.com/prettier/prettier), License: MIT
|
||||
- [protractor@7.0.0](https://github.com/angular/protractor), License: MIT
|
||||
- [resize-observer-polyfill@1.5.1](https://github.com/que-etc/resize-observer-polyfill), License: MIT
|
||||
- [ts-node@9.0.0](https://github.com/TypeStrong/ts-node), License: MIT
|
||||
- [tslint@6.1.3](https://github.com/palantir/tslint), License: Apache-2.0
|
||||
- [tsutils@3.17.1](https://github.com/ajafff/tsutils), License: MIT
|
||||
- [typescript@4.3.5](https://github.com/Microsoft/TypeScript), License: Apache-2.0
|
|
@ -0,0 +1,174 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"client": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "os",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "../server/openslides/static",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": [
|
||||
"src/assets",
|
||||
"src/manifest.json",
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/tinymce",
|
||||
"output": "/tinymce/"
|
||||
},
|
||||
{
|
||||
"glob": "pdf.worker.min.js",
|
||||
"input": "node_modules/pdfjs-dist/build/",
|
||||
"output": "/assets/js/"
|
||||
}
|
||||
],
|
||||
"styles": ["src/styles.scss"],
|
||||
"scripts": [
|
||||
"node_modules/tinymce/tinymce.min.js",
|
||||
"node_modules/video.js/dist/video.min.js",
|
||||
"src/assets/jitsi/external_api.js"
|
||||
],
|
||||
"webWorkerTsConfig": "tsconfig.worker.json",
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"buildOptimizer": false,
|
||||
"sourceMap": true,
|
||||
"optimization": false,
|
||||
"namedChunks": true
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"serviceWorker": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "5mb",
|
||||
"maximumError": "10mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb"
|
||||
}
|
||||
]
|
||||
},
|
||||
"es5": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb"
|
||||
}
|
||||
],
|
||||
"tsConfig": "./tsconfig-es5.app.json"
|
||||
},
|
||||
"development": {}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "client:build:production"
|
||||
},
|
||||
"es5": {
|
||||
"browserTarget": "client:build:es5"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "client:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "client:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"styles": ["src/styles.scss"],
|
||||
"scripts": ["node_modules/tinymce/tinymce.min.js"],
|
||||
"assets": [
|
||||
"src/assets",
|
||||
"src/manifest.json",
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/tinymce/skins",
|
||||
"output": "/tinymce/skins/"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/tinymce/themes",
|
||||
"output": "/tinymce/themes/"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/tinymce/plugins",
|
||||
"output": "/tinymce/plugins/"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"format": "stylish",
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "client:serve:production"
|
||||
},
|
||||
"development": {
|
||||
"devServerTarget": "client:serve:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "client"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
../server/build.sh
|
|
@ -0,0 +1,24 @@
|
|||
FROM node:16 AS nodejs
|
||||
|
||||
RUN mkdir -p /build/app
|
||||
WORKDIR /build/app
|
||||
RUN useradd -m openslides
|
||||
RUN chown -R openslides /build/app
|
||||
|
||||
USER root
|
||||
RUN npm install -g @angular/cli@^12
|
||||
RUN ng config -g cli.warnings.versionMismatch false
|
||||
|
||||
USER openslides
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm -v
|
||||
RUN npm ci
|
||||
COPY .browserslistrc *.json ./
|
||||
COPY src ./src
|
||||
RUN npm run build-to-dir /build/app/static
|
||||
|
||||
COPY docker/client-version.txt static/
|
||||
|
||||
FROM nginx
|
||||
COPY --from=nodejs /build/app/static /usr/share/nginx/html
|
||||
COPY docker/nginx.conf /etc/nginx/nginx.conf
|
|
@ -0,0 +1,12 @@
|
|||
FROM node:16
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json .
|
||||
COPY package-lock.json .
|
||||
RUN npm ci
|
||||
RUN npm run postinstall
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD npm start
|
|
@ -0,0 +1,33 @@
|
|||
worker_processes auto;
|
||||
|
||||
events {
|
||||
worker_connections 32000;
|
||||
}
|
||||
|
||||
http {
|
||||
server {
|
||||
listen 4200;
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
# Optimizations for OpenSlides
|
||||
client_max_body_size 100M;
|
||||
proxy_connect_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
|
||||
proxy_set_header Host $http_host;
|
||||
|
||||
gzip on;
|
||||
gzip_min_length 1000;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: ['./src/**/*.e2e-spec.ts'],
|
||||
capabilities: {
|
||||
browserName: 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.e2e.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
import { AppPage } from './app.po';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getParagraphText()).toEqual('Welcome to client!');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo() {
|
||||
return browser.get('/');
|
||||
}
|
||||
|
||||
getParagraphText() {
|
||||
return element(by.css('os-root h1')).getText();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/app",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": ["jasmine", "jasminewd2", "node"]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client: {
|
||||
clearContext: false, // leave Jasmine Spec Runner output visible in browser
|
||||
jasmine: {
|
||||
// you can add configuration options for Jasmine here
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||
// for example, you can disable the random execution with `random: false`
|
||||
// or set a specific seed with `seed: 4321`
|
||||
},
|
||||
},
|
||||
jasmineHtmlReporter: {
|
||||
suppressAll: true // removes the duplicated traces
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/Test'),
|
||||
subdir: '.',
|
||||
reporters: [{ type: 'html' }, { type: 'text-summary' }]
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
customLaunchers: {
|
||||
ChromeHeadlessNoSandbox: {
|
||||
base: 'ChromeHeadless',
|
||||
flags: ['--no-sandbox']
|
||||
}
|
||||
},
|
||||
singleRun: false,
|
||||
restartOnFileChange: true
|
||||
});
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"index": "/index.html",
|
||||
"assetGroups": [
|
||||
{
|
||||
"name": "app",
|
||||
"installMode": "prefetch",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/favicon.png",
|
||||
"/index.html",
|
||||
"/*.css",
|
||||
"/*.js",
|
||||
"/fira-sans*",
|
||||
"/Material-Icons-Baseline.*"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assets",
|
||||
"installMode": "lazy",
|
||||
"updateMode": "prefetch",
|
||||
"resources": {
|
||||
"files": [
|
||||
"/assets/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"dataGroups": [
|
||||
{
|
||||
"name": "api",
|
||||
"urls": ["/rest/*", "/apps/*", "/system/*", "/stats"],
|
||||
"cacheConfig": {
|
||||
"maxSize": 0,
|
||||
"maxAge": "0u",
|
||||
"strategy": "freshness"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,117 @@
|
|||
{
|
||||
"name": "OpenSlides3-Client",
|
||||
"version": "3.4.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/OpenSlides/OpenSlides.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"description": "OpenSlides 3.0 (Client)",
|
||||
"README": "https://github.com/OpenSlides/OpenSlides/blob/master/client/README.md",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"ng-high-memory": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng",
|
||||
"start": "npm run ng-high-memory -- serve --proxy-config proxy.conf.json --host=0.0.0.0",
|
||||
"start-https": "npm run ng-high-memory -- serve --ssl --ssl-cert localhost.pem --ssl-key localhost-key.pem --proxy-config proxy.conf.json --host=0.0.0.0",
|
||||
"start-es5": "npm run ng-high-memory -- serve --proxy-config proxy.conf.json --host=0.0.0.0 --configuration es5",
|
||||
"build": "npm run ng-high-memory -- build --configuration production",
|
||||
"build-to-dir": "npm run build -- --output-path",
|
||||
"postinstall": "ngcc",
|
||||
"build-debug": "npm run ng-high-memory -- build",
|
||||
"test": "npm run ng-high-memory -- test",
|
||||
"test-silently": "npm run test -- --watch=false --no-progress --browsers=ChromeHeadlessNoSandbox",
|
||||
"test-live": "npm run test -- --watch=true --browsers=ChromeHeadlessNoSandbox",
|
||||
"lint-check": "ng lint",
|
||||
"lint-write": "ng lint --fix",
|
||||
"e2e": "ng e2e",
|
||||
"licenses": "node src/crawler.js",
|
||||
"doc-serve": "./node_modules/.bin/compodoc -c .compodocrc.json -s -o -r",
|
||||
"doc-build": "./node_modules/.bin/compodoc -c .compodocrc.json",
|
||||
"extract": "ngx-translate-extract -i ./src -o ./src/assets/i18n/template-en.pot --clean --sort --format pot",
|
||||
"po2json": "./node_modules/.bin/po2json -f mf src/assets/i18n/de.po src/assets/i18n/de.json && ./node_modules/.bin/po2json -f mf src/assets/i18n/cs.po src/assets/i18n/cs.json && ./node_modules/.bin/po2json -f mf src/assets/i18n/ru.po src/assets/i18n/ru.json",
|
||||
"po2json-tempfix": "./node_modules/.bin/po2json -f mf src/assets/i18n/de.po /dev/stdout | sed -f sed_replacements > src/assets/i18n/de.json && ./node_modules/.bin/po2json -f mf src/assets/i18n/cs.po /dev/stdout | sed -f sed_replacements > src/assets/i18n/cs.json && ./node_modules/.bin/po2json -f mf src/assets/i18n/ru.po /dev/stdout | sed -f sed_replacements > src/assets/i18n/ru.json && ./node_modules/.bin/po2json -f mf src/assets/i18n/it.po /dev/stdout | sed -f sed_replacements > src/assets/i18n/it.json && ./node_modules/.bin/po2json -f mf src/assets/i18n/es.po /dev/stdout | sed -f sed_replacements > src/assets/i18n/es.json",
|
||||
"prettify-check": "prettier --config ./.prettierrc --list-different \"src/{app,environments}/**/*{.ts,.js,.json,.css,.scss}\"",
|
||||
"prettify-write": "prettier --config ./.prettierrc --write \"src/{app,environments}/**/*{.ts,.js,.json,.css,.scss}\"",
|
||||
"cleanup": "npm run prettify-write; npm run lint-write",
|
||||
"cleanup-win": "npm run prettify-write & npm run lint-write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "~12.1.1",
|
||||
"@angular/cdk": "~12.1.1",
|
||||
"@angular/cdk-experimental": "~12.1.1",
|
||||
"@angular/common": "~12.1.1",
|
||||
"@angular/compiler": "~12.1.1",
|
||||
"@angular/core": "~12.1.1",
|
||||
"@angular/forms": "~12.1.1",
|
||||
"@angular/material": "~12.1.1",
|
||||
"@angular/material-moment-adapter": "~12.1.1",
|
||||
"@angular/platform-browser": "~12.1.1",
|
||||
"@angular/platform-browser-dynamic": "~12.1.1",
|
||||
"@angular/router": "~12.1.1",
|
||||
"@angular/service-worker": "~12.1.1",
|
||||
"@ngx-pwa/local-storage": "~12.0.0",
|
||||
"@ngx-translate/core": "~13.0.0",
|
||||
"@ngx-translate/http-loader": "^6.0.0",
|
||||
"@pebula/ngrid": "4.0.0-alpha.3",
|
||||
"@pebula/ngrid-material": "4.0.0-alpha.3",
|
||||
"@pebula/utils": "1.0.2",
|
||||
"@tinymce/tinymce-angular": "^4.2.4",
|
||||
"@videojs/http-streaming": "^2.9.1",
|
||||
"acorn": "^8.0.1",
|
||||
"chart.js": "^2.9.3",
|
||||
"core-js": "^3.6.5",
|
||||
"css-element-queries": "^1.2.3",
|
||||
"exceljs": "4.1.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"jszip": "^3.7.1",
|
||||
"lz4js": "^0.2.0",
|
||||
"material-design-icons-iconfont": "6.1.0",
|
||||
"moment": "^2.27.0",
|
||||
"ng-particles": "^2.6.0",
|
||||
"ng2-charts": "^2.4.0",
|
||||
"ng2-pdf-viewer": "^6.4.1",
|
||||
"ngx-device-detector": "^2.0.0",
|
||||
"ngx-file-drop": "^11.1.0",
|
||||
"ngx-mat-select-search": "^3.3.0",
|
||||
"ngx-material-timepicker": "^5.5.3",
|
||||
"ngx-papaparse": "^4.0.4",
|
||||
"pdfjs-dist": "2.5.207",
|
||||
"pdfmake": "^0.1.68",
|
||||
"po2json": "^1.0.0-beta-2",
|
||||
"rxjs": "^6.6.2",
|
||||
"tinymce": "5.7.1",
|
||||
"tslib": "^2.0.0",
|
||||
"tsparticles": "^1.23.0",
|
||||
"video.js": "^7.8.4",
|
||||
"zone.js": "~0.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~12.1.1",
|
||||
"@angular-devkit/schematics": "^12.1.1",
|
||||
"@angular/cli": "~12.1.1",
|
||||
"@angular/compiler-cli": "~12.1.1",
|
||||
"@angular/language-service": "~12.1.1",
|
||||
"@biesbjerg/ngx-translate-extract": "^7.0.4",
|
||||
"@biesbjerg/ngx-translate-extract-marker": "^1.0.0",
|
||||
"@compodoc/compodoc": "^1.0.9",
|
||||
"@schematics/angular": "^10.0.8",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/node": "^14.6.2",
|
||||
"@types/yargs": "^15.0.4",
|
||||
"codelyzer": "^6.0.0",
|
||||
"husky": "^4.2.3",
|
||||
"jasmine-core": "~3.8.0",
|
||||
"karma": "^6.3.4",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage": "~2.0.3",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"prettier": "2.3.2",
|
||||
"protractor": "^7.0.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"ts-node": "~9.0.0",
|
||||
"tslint": "~6.1.3",
|
||||
"tsutils": "3.17.1",
|
||||
"typescript": "~4.3.5"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"/apps/": {
|
||||
"target": "http://localhost:8000",
|
||||
"secure": false
|
||||
},
|
||||
"/media/": {
|
||||
"target": "http://localhost:8000",
|
||||
"secure": false
|
||||
},
|
||||
"/rest/": {
|
||||
"target": "http://localhost:8000",
|
||||
"secure": false
|
||||
},
|
||||
"/ws/": {
|
||||
"target": "ws://localhost:8000",
|
||||
"secure": false,
|
||||
"ws": true
|
||||
},
|
||||
"/system/": {
|
||||
"target": "https://localhost:8002",
|
||||
"secure": false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
s/\\\\{/{/g
|
||||
s/\\\\}/}/g
|
||||
s/{0}ser%/%user%/g
|
||||
s/{0}um%/%num%/g
|
|
@ -0,0 +1,13 @@
|
|||
import { AppRoutingModule } from './app-routing.module';
|
||||
|
||||
describe('AppRoutingModule', () => {
|
||||
let appRoutingModule: AppRoutingModule;
|
||||
|
||||
beforeEach(() => {
|
||||
appRoutingModule = new AppRoutingModule();
|
||||
});
|
||||
|
||||
it('should create an instance', () => {
|
||||
expect(appRoutingModule).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { Route, RouterModule } from '@angular/router';
|
||||
|
||||
import { LoginLegalNoticeComponent } from './site/login/components/login-legal-notice/login-legal-notice.component';
|
||||
import { LoginMaskComponent } from './site/login/components/login-mask/login-mask.component';
|
||||
import { LoginPrivacyPolicyComponent } from './site/login/components/login-privacy-policy/login-privacy-policy.component';
|
||||
import { LoginWrapperComponent } from './site/login/components/login-wrapper/login-wrapper.component';
|
||||
import { ResetPasswordConfirmComponent } from './site/login/components/reset-password-confirm/reset-password-confirm.component';
|
||||
import { ResetPasswordComponent } from './site/login/components/reset-password/reset-password.component';
|
||||
import { UnsupportedBrowserComponent } from './site/login/components/unsupported-browser/unsupported-browser.component';
|
||||
|
||||
/**
|
||||
* Global app routing
|
||||
*/
|
||||
const routes: Route[] = [
|
||||
{
|
||||
path: 'login',
|
||||
component: LoginWrapperComponent,
|
||||
children: [
|
||||
{ path: '', component: LoginMaskComponent, pathMatch: 'full' },
|
||||
{ path: 'reset-password', component: ResetPasswordComponent },
|
||||
{ path: 'reset-password-confirm', component: ResetPasswordConfirmComponent },
|
||||
{ path: 'legalnotice', component: LoginLegalNoticeComponent },
|
||||
{ path: 'privacypolicy', component: LoginPrivacyPolicyComponent },
|
||||
{ path: 'unsupported-browser', component: UnsupportedBrowserComponent }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'projector',
|
||||
loadChildren: () =>
|
||||
import('./fullscreen-projector/fullscreen-projector.module').then(m => m.FullscreenProjectorModule),
|
||||
data: { noInterruption: true }
|
||||
},
|
||||
{ path: '', loadChildren: () => import('./site/site.module').then(m => m.SiteModule) },
|
||||
{ path: '**', redirectTo: '' }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes, { onSameUrlNavigation: 'reload', relativeLinkResolution: 'legacy' })],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule {}
|
|
@ -0,0 +1,3 @@
|
|||
<div class="content">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
|
@ -0,0 +1,4 @@
|
|||
.content {
|
||||
flex: 1;
|
||||
height: 100vh;
|
||||
}
|
|
@ -0,0 +1,247 @@
|
|||
import { ApplicationRef, Component } from '@angular/core';
|
||||
import { MatIconRegistry } from '@angular/material/icon';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { first, tap } from 'rxjs/operators';
|
||||
|
||||
import { ChatNotificationService } from './site/chat/services/chat-notification.service';
|
||||
import { ConfigService } from './core/ui-services/config.service';
|
||||
import { ConstantsService } from './core/core-services/constants.service';
|
||||
import { CountUsersService } from './core/ui-services/count-users.service';
|
||||
import { DataStoreUpgradeService } from './core/core-services/data-store-upgrade.service';
|
||||
import { LoadFontService } from './core/ui-services/load-font.service';
|
||||
import { LoginDataService } from './core/ui-services/login-data.service';
|
||||
import { OfflineService } from './core/core-services/offline.service';
|
||||
import { OpenSlidesStatusService } from './core/core-services/openslides-status.service';
|
||||
import { OpenSlidesService } from './core/core-services/openslides.service';
|
||||
import { OperatorService } from './core/core-services/operator.service';
|
||||
import { OverlayService } from './core/ui-services/overlay.service';
|
||||
import { RoutingStateService } from './core/ui-services/routing-state.service';
|
||||
import { ServertimeService } from './core/core-services/servertime.service';
|
||||
import { ThemeService } from './core/ui-services/theme.service';
|
||||
import { VotingBannerService } from './core/ui-services/voting-banner.service';
|
||||
|
||||
declare global {
|
||||
/**
|
||||
* Enhance array with own functions
|
||||
* TODO: Remove once flatMap made its way into official JS/TS (ES 2019?)
|
||||
*/
|
||||
interface Array<T> {
|
||||
flatMap(o: any): any[];
|
||||
intersect(a: T[]): T[];
|
||||
mapToObject(f: (item: T) => { [key: string]: any }): { [key: string]: any };
|
||||
}
|
||||
|
||||
interface Set<T> {
|
||||
equals(other: Set<T>): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances the number object to calculate real modulo operations.
|
||||
* (not remainder)
|
||||
*/
|
||||
interface Number {
|
||||
modulo(n: number): number;
|
||||
}
|
||||
|
||||
interface String {
|
||||
decode(): string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* May only be created once since this thing is fat
|
||||
*/
|
||||
const domParser = new DOMParser();
|
||||
|
||||
/**
|
||||
* Angular's global App Component
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent {
|
||||
/**
|
||||
* Master-component of all apps.
|
||||
*
|
||||
* Inits the translation service, the operator, the login data and the constants.
|
||||
*
|
||||
* Handles the altering of Array.toString()
|
||||
*
|
||||
* Most of the injected service are not used - this is ok. It is needed to definitly
|
||||
* run their constructors at app loading time
|
||||
*/
|
||||
public constructor(
|
||||
private matIconRegistry: MatIconRegistry,
|
||||
private domSanitizer: DomSanitizer,
|
||||
translate: TranslateService,
|
||||
appRef: ApplicationRef,
|
||||
servertimeService: ServertimeService,
|
||||
openslidesService: OpenSlidesService,
|
||||
openslidesStatus: OpenSlidesStatusService,
|
||||
router: Router,
|
||||
offlineService: OfflineService,
|
||||
operator: OperatorService,
|
||||
loginDataService: LoginDataService,
|
||||
constantsService: ConstantsService,
|
||||
themeService: ThemeService,
|
||||
overlayService: OverlayService,
|
||||
countUsersService: CountUsersService, // Needed to register itself.
|
||||
configService: ConfigService,
|
||||
loadFontService: LoadFontService,
|
||||
dataStoreUpgradeService: DataStoreUpgradeService, // to start it.
|
||||
routingState: RoutingStateService,
|
||||
votingBannerService: VotingBannerService, // needed for initialisation,
|
||||
chatNotificationService: ChatNotificationService
|
||||
) {
|
||||
// manually add the supported languages
|
||||
translate.addLangs(['en', 'de', 'it', 'es', 'cs', 'ru']);
|
||||
// this language will be used as a fallback when a translation isn't found in the current language
|
||||
translate.setDefaultLang('en');
|
||||
// get the browsers default language
|
||||
const browserLang = translate.getBrowserLang();
|
||||
// try to use the browser language if it is available. If not, uses english.
|
||||
translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en');
|
||||
|
||||
// change default JS functions
|
||||
this.overloadArrayFunctions();
|
||||
this.overloadSetFunctions();
|
||||
this.overloadModulo();
|
||||
this.loadCustomIcons();
|
||||
this.overloadDecodeString();
|
||||
|
||||
// Wait until the App reaches a stable state.
|
||||
// Required for the Service Worker.
|
||||
appRef.isStable
|
||||
.pipe(
|
||||
// take only the stable state
|
||||
first(stable => stable),
|
||||
tap(() => console.debug('App is now stable!'))
|
||||
)
|
||||
.subscribe(() => {
|
||||
openslidesStatus.setStable();
|
||||
servertimeService.startScheduler();
|
||||
});
|
||||
}
|
||||
|
||||
private overloadArrayFunctions(): void {
|
||||
Object.defineProperty(Array.prototype, 'toString', {
|
||||
value: function (): string {
|
||||
let string = '';
|
||||
const iterations = Math.min(this.length, 3);
|
||||
|
||||
for (let i = 0; i <= iterations; i++) {
|
||||
if (i < iterations) {
|
||||
string += this[i];
|
||||
}
|
||||
|
||||
if (i < iterations - 1) {
|
||||
string += ', ';
|
||||
} else if (i === iterations && this.length > iterations) {
|
||||
string += ', ...';
|
||||
}
|
||||
}
|
||||
return string;
|
||||
},
|
||||
enumerable: false
|
||||
});
|
||||
|
||||
Object.defineProperty(Array.prototype, 'flatMap', {
|
||||
value: function (o: any): any[] {
|
||||
const concatFunction = (x: any, y: any[]) => x.concat(y);
|
||||
const flatMapLogic = (f: any, xs: any) => xs.map(f).reduce(concatFunction, []);
|
||||
return flatMapLogic(o, this);
|
||||
},
|
||||
enumerable: false
|
||||
});
|
||||
|
||||
Object.defineProperty(Array.prototype, 'intersect', {
|
||||
value: function <T>(other: T[]): T[] {
|
||||
let a = this;
|
||||
let b = other;
|
||||
// indexOf to loop over shorter
|
||||
if (b.length > a.length) {
|
||||
[a, b] = [b, a];
|
||||
}
|
||||
return a.filter(e => b.indexOf(e) > -1);
|
||||
},
|
||||
enumerable: false
|
||||
});
|
||||
|
||||
Object.defineProperty(Array.prototype, 'mapToObject', {
|
||||
value: function <T>(f: (item: T) => { [key: string]: any }): { [key: string]: any } {
|
||||
return this.reduce((aggr, item) => {
|
||||
const res = f(item);
|
||||
for (const key in res) {
|
||||
if (res.hasOwnProperty(key)) {
|
||||
aggr[key] = res[key];
|
||||
}
|
||||
}
|
||||
return aggr;
|
||||
}, {});
|
||||
},
|
||||
enumerable: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds some functions to Set.
|
||||
*/
|
||||
private overloadSetFunctions(): void {
|
||||
Object.defineProperty(Set.prototype, 'equals', {
|
||||
value: function <T>(other: Set<T>): boolean {
|
||||
const difference = new Set(this);
|
||||
for (const elem of other) {
|
||||
if (difference.has(elem)) {
|
||||
difference.delete(elem);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !difference.size;
|
||||
},
|
||||
enumerable: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is not the fastest solution but the most reliable one.
|
||||
* Certain languages and TinyMCE do not follow the any predictable
|
||||
* behaviour when it comes to encoding UTF8.
|
||||
* decodeURI and decodeURIComponent were not able to successfully
|
||||
* replace any ;&*uml with something meaningfull.
|
||||
*/
|
||||
private overloadDecodeString(): void {
|
||||
Object.defineProperty(String.prototype, 'decode', {
|
||||
enumerable: false,
|
||||
value(): string {
|
||||
const doc = domParser.parseFromString(this, 'text/html');
|
||||
return doc.body.textContent || '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances the number object with a real modulo operation (not remainder).
|
||||
* TODO: Remove this, if the remainder operation is changed to modulo.
|
||||
*/
|
||||
private overloadModulo(): void {
|
||||
Object.defineProperty(Number.prototype, 'modulo', {
|
||||
value: function (n: number): number {
|
||||
return ((this % n) + n) % n;
|
||||
},
|
||||
enumerable: false
|
||||
});
|
||||
}
|
||||
|
||||
private loadCustomIcons(): void {
|
||||
this.matIconRegistry.addSvgIcon(
|
||||
`clapping_hands`,
|
||||
this.domSanitizer.bypassSecurityTrustResourceUrl('../assets/svg/clapping_hands.svg')
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http';
|
||||
import { APP_INITIALIZER, ErrorHandler, NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
|
||||
import { StorageModule } from '@ngx-pwa/local-storage';
|
||||
|
||||
import { AppLoadService } from './core/core-services/app-load.service';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { CoreModule } from './core/core.module';
|
||||
import { environment } from '../environments/environment';
|
||||
import { ErrorService } from './core/core-services/error.service';
|
||||
import { httpInterceptorProviders } from './core/core-services/http-interceptors';
|
||||
import { LoginModule } from './site/login/login.module';
|
||||
import { OpenSlidesTranslateModule } from './core/translate/openslides-translate-module';
|
||||
import { SlidesModule } from './slides/slides.module';
|
||||
|
||||
/**
|
||||
* Returns a function that returns a promis that will be resolved, if all apps are loaded.
|
||||
* @param appLoadService The service that loads the apps.
|
||||
*/
|
||||
export function AppLoaderFactory(appLoadService: AppLoadService): () => Promise<void> {
|
||||
return () => appLoadService.loadApps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Global App Module. Keep it as clean as possible.
|
||||
*/
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
HttpClientModule,
|
||||
HttpClientXsrfModule.withOptions({
|
||||
cookieName: 'OpenSlidesCsrfToken',
|
||||
headerName: 'X-CSRFToken'
|
||||
}),
|
||||
BrowserAnimationsModule,
|
||||
OpenSlidesTranslateModule.forRoot(),
|
||||
AppRoutingModule,
|
||||
CoreModule,
|
||||
LoginModule,
|
||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
||||
SlidesModule.forRoot(),
|
||||
StorageModule.forRoot({ IDBNoWrap: false })
|
||||
],
|
||||
providers: [
|
||||
{ provide: APP_INITIALIZER, useFactory: AppLoaderFactory, deps: [AppLoadService], multi: true },
|
||||
httpInterceptorProviders,
|
||||
{ provide: ErrorHandler, useClass: ErrorService }
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule {}
|
|
@ -0,0 +1,140 @@
|
|||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { Permission } from './core/core-services/operator.service';
|
||||
|
||||
/**
|
||||
* Provides functionalities that will be used by most components
|
||||
* currently able to set the title with the suffix ' - OpenSlides'
|
||||
*
|
||||
* A BaseComponent is an OpenSlides Component.
|
||||
* Components in the 'Side'- or 'projector' Folder are BaseComponents
|
||||
*/
|
||||
export abstract class BaseComponent {
|
||||
/**
|
||||
* To check permissions in templates using permission.[...]
|
||||
*/
|
||||
public permission = Permission;
|
||||
|
||||
/**
|
||||
* To manipulate the browser title bar, adds the Suffix "OpenSlides"
|
||||
*
|
||||
* Might be a config variable later at some point
|
||||
*/
|
||||
private titleSuffix = ' - OpenSlides';
|
||||
|
||||
/**
|
||||
* Holds the coordinates where a swipe gesture was used
|
||||
*/
|
||||
protected swipeCoord?: [number, number];
|
||||
|
||||
/**
|
||||
* Holds the time when the user was swiping
|
||||
*/
|
||||
protected swipeTime?: number;
|
||||
|
||||
/**
|
||||
* Determine to display a save hint
|
||||
*/
|
||||
public saveHint: boolean;
|
||||
|
||||
/**
|
||||
* Settings for the TinyMCE editor selector
|
||||
*/
|
||||
public tinyMceSettings = {
|
||||
base_url: '/tinymce', // Root for resources
|
||||
suffix: '.min', // Suffix to use when loading resources
|
||||
theme: 'silver',
|
||||
language: null,
|
||||
language_url: null,
|
||||
inline: false,
|
||||
statusbar: false,
|
||||
browser_spellcheck: true,
|
||||
image_advtab: true,
|
||||
image_description: false,
|
||||
link_title: false,
|
||||
height: 320,
|
||||
plugins: `autolink charmap code fullscreen image imagetools
|
||||
lists link paste searchreplace`,
|
||||
menubar: false,
|
||||
contextmenu: false,
|
||||
toolbar: `styleselect | bold italic underline strikethrough |
|
||||
forecolor backcolor removeformat | bullist numlist |
|
||||
link image charmap | code fullscreen`,
|
||||
mobile: {
|
||||
theme: 'mobile',
|
||||
plugins: ['autosave', 'lists', 'autolink']
|
||||
},
|
||||
relative_urls: false,
|
||||
remove_script_host: true,
|
||||
paste_preprocess: this.pastePreprocess
|
||||
};
|
||||
|
||||
public constructor(protected titleService: Title, protected translate: TranslateService) {
|
||||
this.tinyMceSettings.language_url = '/assets/tinymce/langs/' + this.translate.currentLang + '.js';
|
||||
this.tinyMceSettings.language = this.translate.currentLang;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean pasted HTML.
|
||||
* If the user decides to copy-paste HTML (like from another OpenSlides motion detail)
|
||||
* - remove all classes
|
||||
* - remove data-line-number="X"
|
||||
* - remove contenteditable="false"
|
||||
*
|
||||
* Not doing so would save control sequences from diff/linenumbering into the
|
||||
* model which will open pandoras pox during PDF generation (and potentially web view)
|
||||
* @param _
|
||||
* @param args
|
||||
*/
|
||||
private pastePreprocess(_: any, args: any): void {
|
||||
const getClassesRe: RegExp = new RegExp(/\s*class\=\"[\w\W]*?\"/, 'gi');
|
||||
const getDataLineNumberRe: RegExp = new RegExp(/\s*data-line-number\=\"\d+\"/, 'gi');
|
||||
const getContentEditableRe: RegExp = new RegExp(/\s*contenteditable\=\"\w+\"/, 'gi');
|
||||
const cleanedContent = (args.content as string)
|
||||
.replace(getClassesRe, '')
|
||||
.replace(getDataLineNumberRe, '')
|
||||
.replace(getContentEditableRe, '');
|
||||
args.content = cleanedContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the title in web browser using angulars TitleService
|
||||
* @param prefix The title prefix. Should be translated here.
|
||||
*/
|
||||
public setTitle(prefix: string): void {
|
||||
const translatedPrefix = this.translate.instant(prefix);
|
||||
this.titleService.setTitle(translatedPrefix + this.titleSuffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for indexed *ngFor components
|
||||
*
|
||||
* @param index
|
||||
*/
|
||||
public trackByIndex(index: number): number {
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* TinyMCE Init callback. Used for certain mobile editors
|
||||
* @param event
|
||||
*/
|
||||
protected onInitTinyMce(event: any): void {
|
||||
console.log('tinyMCE event: ', event);
|
||||
|
||||
if (event.event.target.settings.theme === 'mobile') {
|
||||
console.log('is mobile editor');
|
||||
this.saveHint = true;
|
||||
} else {
|
||||
console.log('is no mobile editor');
|
||||
event.editor.focus();
|
||||
}
|
||||
}
|
||||
|
||||
protected onLeaveTinyMce(event: any): void {
|
||||
console.log('tinyevent:', event.event.type);
|
||||
this.saveHint = false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AppLoadService } from './app-load.service';
|
||||
import { E2EImportsModule } from '../../../e2e-imports.module';
|
||||
|
||||
describe('AppLoadService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [AppLoadService]
|
||||
});
|
||||
});
|
||||
it('should be created', inject([AppLoadService], (service: AppLoadService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,117 @@
|
|||
import { Injectable, Injector } from '@angular/core';
|
||||
|
||||
import { AgendaAppConfig } from '../../site/agenda/agenda.config';
|
||||
import { AppConfig, ModelEntry, SearchableModelEntry } from '../definitions/app-config';
|
||||
import { BaseRepository } from 'app/core/repositories/base-repository';
|
||||
import { ChatAppConfig } from 'app/site/chat/chat.config';
|
||||
import { CinemaAppConfig } from 'app/site/cinema/cinema.config';
|
||||
import { HistoryAppConfig } from 'app/site/history/history.config';
|
||||
import { ProjectorAppConfig } from 'app/site/projector/projector.config';
|
||||
import { TopicsAppConfig } from 'app/site/topics/topics.config';
|
||||
import { AssignmentsAppConfig } from '../../site/assignments/assignments.config';
|
||||
import { CollectionStringMapperService } from './collection-string-mapper.service';
|
||||
import { CommonAppConfig } from '../../site/common/common.config';
|
||||
import { ConfigAppConfig } from '../../site/config/config.config';
|
||||
import { ServicesToLoadOnAppsLoaded } from '../core.module';
|
||||
import { FallbackRoutesService } from './fallback-routes.service';
|
||||
import { MainMenuService } from './main-menu.service';
|
||||
import { MediafileAppConfig } from '../../site/mediafiles/mediafile.config';
|
||||
import { MotionsAppConfig } from '../../site/motions/motions.config';
|
||||
import { OnAfterAppsLoaded } from '../definitions/on-after-apps-loaded';
|
||||
import { SearchService } from '../ui-services/search.service';
|
||||
import { isSearchable } from '../../site/base/searchable';
|
||||
import { TagAppConfig } from '../../site/tags/tag.config';
|
||||
import { UsersAppConfig } from '../../site/users/users.config';
|
||||
|
||||
/**
|
||||
* A list of all app configurations of all delivered apps.
|
||||
*/
|
||||
const appConfigs: AppConfig[] = [
|
||||
CommonAppConfig,
|
||||
ConfigAppConfig,
|
||||
AgendaAppConfig,
|
||||
AssignmentsAppConfig,
|
||||
MotionsAppConfig,
|
||||
MediafileAppConfig,
|
||||
TagAppConfig,
|
||||
UsersAppConfig,
|
||||
HistoryAppConfig,
|
||||
ProjectorAppConfig,
|
||||
TopicsAppConfig,
|
||||
CinemaAppConfig,
|
||||
ChatAppConfig
|
||||
];
|
||||
|
||||
/**
|
||||
* Handles loading of all apps during the bootup process.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AppLoadService {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param modelMapper
|
||||
* @param mainMenuService
|
||||
* @param searchService
|
||||
*/
|
||||
public constructor(
|
||||
private modelMapper: CollectionStringMapperService,
|
||||
private mainMenuService: MainMenuService,
|
||||
private searchService: SearchService,
|
||||
private injector: Injector,
|
||||
private fallbackRoutesService: FallbackRoutesService
|
||||
) {}
|
||||
|
||||
public async loadApps(): Promise<void> {
|
||||
const repositories: OnAfterAppsLoaded[] = [];
|
||||
appConfigs.forEach((config: AppConfig) => {
|
||||
if (config.models) {
|
||||
config.models.forEach(entry => {
|
||||
let repository: BaseRepository<any, any, any> = null;
|
||||
repository = this.injector.get(entry.repository);
|
||||
repositories.push(repository);
|
||||
this.modelMapper.registerCollectionElement(entry.model, entry.viewModel, repository);
|
||||
if (this.isSearchableModelEntry(entry)) {
|
||||
this.searchService.registerModel(
|
||||
entry.model.COLLECTIONSTRING,
|
||||
repository,
|
||||
entry.searchOrder,
|
||||
entry.openInNewTab
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (config.mainMenuEntries) {
|
||||
this.mainMenuService.registerEntries(config.mainMenuEntries);
|
||||
this.fallbackRoutesService.registerFallbackEntries(config.mainMenuEntries);
|
||||
}
|
||||
});
|
||||
|
||||
// Collect all services to notify for the OnAfterAppsLoadedHook
|
||||
const onAfterAppsLoadedItems = ServicesToLoadOnAppsLoaded.map(service => {
|
||||
return this.injector.get(service);
|
||||
}).concat(repositories);
|
||||
|
||||
// Notify them.
|
||||
onAfterAppsLoadedItems.forEach(repo => {
|
||||
repo.onAfterAppsLoaded();
|
||||
});
|
||||
}
|
||||
|
||||
private isSearchableModelEntry(entry: ModelEntry | SearchableModelEntry): entry is SearchableModelEntry {
|
||||
if ((<SearchableModelEntry>entry).searchOrder !== undefined) {
|
||||
// We need to double check, because Typescipt cannot check contructors. If typescript could differentiate
|
||||
// between (ModelConstructor<BaseModel>) and (new (...args: any[]) => (BaseModel & Searchable)),
|
||||
// we would not have to check if the result of the contructor (the model instance) is really a searchable.
|
||||
if (!isSearchable(new entry.viewModel())) {
|
||||
throw Error(
|
||||
`Wrong configuration for ${entry.model.COLLECTIONSTRING}: you gave a searchOrder, but the model is not searchable.`
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router } from '@angular/router';
|
||||
|
||||
import { FallbackRoutesService } from './fallback-routes.service';
|
||||
import { OpenSlidesService } from './openslides.service';
|
||||
import { OperatorService, Permission } from './operator.service';
|
||||
|
||||
/**
|
||||
* Classical Auth-Guard. Checks if the user has to correct permissions to enter a page, and forwards to login if not.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthGuard implements CanActivate, CanActivateChild {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param router To navigate to a target URL
|
||||
* @param operator Asking for the required permission
|
||||
* @param openSlidesService Handle OpenSlides functions
|
||||
*/
|
||||
public constructor(
|
||||
private router: Router,
|
||||
private operator: OperatorService,
|
||||
private openSlidesService: OpenSlidesService,
|
||||
private fallbackRoutesService: FallbackRoutesService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Checks of the operator has the required permission to see the state.
|
||||
*
|
||||
* One can set extra data to the state with `data: {basePerm: '<perm>'}` or
|
||||
* `data: {basePerm: ['<perm1>', '<perm2>']}` to lock the access to users
|
||||
* only with the given permission(s).
|
||||
*
|
||||
* @param route the route the user wants to navigate to
|
||||
*/
|
||||
public canActivate(route: ActivatedRouteSnapshot): boolean {
|
||||
const basePerm: Permission | Permission[] = route.data.basePerm;
|
||||
|
||||
if (!basePerm) {
|
||||
return true;
|
||||
} else if (basePerm instanceof Array) {
|
||||
return this.operator.hasPerms(...basePerm);
|
||||
} else {
|
||||
return this.operator.hasPerms(basePerm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@method canActivate}. Should have the same logic.
|
||||
*
|
||||
* @param route the route the user wants to navigate to
|
||||
*/
|
||||
public async canActivateChild(route: ActivatedRouteSnapshot): Promise<boolean> {
|
||||
await this.operator.loaded;
|
||||
|
||||
if (this.canActivate(route)) {
|
||||
return true;
|
||||
} else {
|
||||
this.handleForbiddenRoute(route);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a forbidden route. If the route is "/" (start page), It is tried to
|
||||
* use a fallback route provided by AuthGuardFallbackRoutes. If this won't work
|
||||
* or it wasn't the start page in the first place, the operator will be redirected
|
||||
* to an error page.
|
||||
*/
|
||||
private handleForbiddenRoute(route: ActivatedRouteSnapshot): void {
|
||||
if (route.url.length === 0) {
|
||||
// start page
|
||||
const fallbackRoute = this.fallbackRoutesService.getFallbackRoute();
|
||||
if (fallbackRoute) {
|
||||
this.router.navigate([fallbackRoute]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Fall-through: If the url is the start page, but no other fallback was found,
|
||||
// navigate to the error page.
|
||||
|
||||
this.openSlidesService.redirectUrl = location.pathname;
|
||||
this.router.navigate(['/error'], {
|
||||
queryParams: {
|
||||
error: 'Authentication Error',
|
||||
msg: route.data.basePerm
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
import { E2EImportsModule } from '../../../e2e-imports.module';
|
||||
|
||||
describe('ConfigService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [AuthService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([AuthService], (service: AuthService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,133 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { environment } from 'environments/environment';
|
||||
|
||||
import { OperatorService, WhoAmI } from 'app/core/core-services/operator.service';
|
||||
import { DEFAULT_AUTH_TYPE, UserAuthType } from 'app/shared/models/users/user';
|
||||
import { DataStoreService } from './data-store.service';
|
||||
import { HttpService } from './http.service';
|
||||
import { OpenSlidesService } from './openslides.service';
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
interface LoginData {
|
||||
username: string;
|
||||
password: string;
|
||||
cookies?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates an OpenSlides user with username and password
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
/**
|
||||
* Initializes the httpClient and the {@link OperatorService}.
|
||||
*
|
||||
* @param http HttpService to send requests to the server
|
||||
* @param operator Who is using OpenSlides
|
||||
* @param OpenSlides The openslides service
|
||||
* @param router To navigate
|
||||
*/
|
||||
public constructor(
|
||||
private http: HttpService,
|
||||
private operator: OperatorService,
|
||||
private OpenSlides: OpenSlidesService,
|
||||
private router: Router,
|
||||
private DS: DataStoreService,
|
||||
private storageService: StorageService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Try to log in a user with a given auth type
|
||||
*
|
||||
* - Type "default": username and password needed; the earlySuccessCallback will be called.
|
||||
* - Type "saml": The windows location will be changed to the single-sign-on service initiator.
|
||||
*/
|
||||
public async login(
|
||||
authType: UserAuthType,
|
||||
username: string,
|
||||
password: string,
|
||||
earlySuccessCallback: () => void
|
||||
): Promise<void> {
|
||||
if (authType === 'default') {
|
||||
const data: LoginData = {
|
||||
username: username,
|
||||
password: password
|
||||
};
|
||||
if (!navigator.cookieEnabled) {
|
||||
data.cookies = false;
|
||||
}
|
||||
const response = await this.http.post<WhoAmI>(environment.urlPrefix + '/users/login/', data);
|
||||
earlySuccessCallback();
|
||||
await this.OpenSlides.shutdown();
|
||||
await this.operator.setWhoAmI(response);
|
||||
await this.OpenSlides.afterLoginBootup(response.user_id);
|
||||
await this.redirectUser(response.user_id);
|
||||
} else if (authType === 'saml') {
|
||||
await this.operator.clearWhoAmIFromStorage(); // This is important:
|
||||
// Then returning to the page, we do not want to have anything cached so a
|
||||
// fresh whoami is executed.
|
||||
window.location.href = environment.urlPrefix + '/saml/?sso'; // Bye
|
||||
} else {
|
||||
throw new Error(`Unsupported auth type "${authType}"`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects the user to the page where he came from. Boots OpenSlides,
|
||||
* if it wasn't done before.
|
||||
*/
|
||||
public async redirectUser(userId: number): Promise<void> {
|
||||
if (!this.OpenSlides.isBooted) {
|
||||
await this.OpenSlides.afterLoginBootup(userId);
|
||||
}
|
||||
|
||||
let redirect = this.OpenSlides.redirectUrl ? this.OpenSlides.redirectUrl : '/';
|
||||
|
||||
const excludedUrls = ['login'];
|
||||
if (excludedUrls.some(url => redirect.includes(url))) {
|
||||
redirect = '/';
|
||||
}
|
||||
this.router.navigate([redirect]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Login for guests.
|
||||
*/
|
||||
public async guestLogin(): Promise<void> {
|
||||
this.redirectUser(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout function for both the client and the server.
|
||||
*
|
||||
* Will clear the datastore, update the current operator and
|
||||
* send a `post`-request to `/apps/users/logout/'`. Restarts OpenSlides.
|
||||
*/
|
||||
public async logout(): Promise<void> {
|
||||
const authType = this.operator.authType.getValue();
|
||||
if (authType === DEFAULT_AUTH_TYPE) {
|
||||
let response = null;
|
||||
try {
|
||||
response = await this.http.post<WhoAmI>(environment.urlPrefix + '/users/logout/', {});
|
||||
} catch (e) {
|
||||
// We do nothing on failures. Reboot OpenSlides anyway.
|
||||
}
|
||||
this.router.navigate(['/']);
|
||||
await this.storageService.clear();
|
||||
await this.DS.clear();
|
||||
await this.operator.setWhoAmI(response);
|
||||
await this.OpenSlides.reboot();
|
||||
} else if (authType === 'saml') {
|
||||
await this.storageService.clear();
|
||||
await this.DS.clear();
|
||||
await this.operator.setWhoAmI(null);
|
||||
window.location.href = environment.urlPrefix + '/saml/?slo'; // Bye
|
||||
} else {
|
||||
throw new Error(`Unsupported auth type "${authType}"`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
import { EventEmitter, Injectable } from '@angular/core';
|
||||
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
|
||||
import { AutoupdateFormat } from 'app/core/definitions/autoupdate-format';
|
||||
import { trailingThrottleTime } from 'app/core/rxjs/trailing-throttle-time';
|
||||
import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||
import { ConstantsService } from './constants.service';
|
||||
import { HttpService } from './http.service';
|
||||
|
||||
interface ThrottleSettings {
|
||||
AUTOUPDATE_DELAY?: number;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AutoupdateThrottleService {
|
||||
private readonly _autoupdatesToInject = new Subject<AutoupdateFormat>();
|
||||
|
||||
public get autoupdatesToInject(): Observable<AutoupdateFormat> {
|
||||
return this._autoupdatesToInject.asObservable();
|
||||
}
|
||||
|
||||
private readonly receivedAutoupdate = new EventEmitter<void>();
|
||||
|
||||
private pendingAutoupdates = [];
|
||||
|
||||
private disabledUntil: number | null = null;
|
||||
|
||||
private delay = 0;
|
||||
|
||||
private maxSeenChangeId = 0;
|
||||
|
||||
private get isActive(): boolean {
|
||||
return this.delay !== 0;
|
||||
}
|
||||
|
||||
public constructor(private constantsService: ConstantsService, private httpService: HttpService) {
|
||||
this.constantsService.get<ThrottleSettings>('Settings').subscribe(settings => {
|
||||
// This is a one-shot. If the delay was set one time >0, it cannot be changed afterwards.
|
||||
// A change is more complicated since you have to unsubscribe, clean pending autoupdates,
|
||||
// subscribe again and make sure, that no autoupdate is missed.
|
||||
if (this.delay === 0 && settings.AUTOUPDATE_DELAY) {
|
||||
this.delay = 1000 * settings.AUTOUPDATE_DELAY;
|
||||
console.log(`Configured autoupdate delay: ${this.delay}ms`);
|
||||
this.receivedAutoupdate
|
||||
.pipe(trailingThrottleTime(this.delay))
|
||||
.subscribe(() => this.processPendingAutoupdates());
|
||||
} else if (this.delay === 0) {
|
||||
console.log('No autoupdate delay');
|
||||
}
|
||||
});
|
||||
|
||||
this.httpService.responseChangeIds.subscribe(changeId => this.disableUntil(changeId));
|
||||
}
|
||||
|
||||
public newAutoupdate(autoupdate: AutoupdateFormat): void {
|
||||
if (autoupdate.to_change_id > this.maxSeenChangeId) {
|
||||
this.maxSeenChangeId = autoupdate.to_change_id;
|
||||
}
|
||||
|
||||
if (!this.isActive) {
|
||||
this._autoupdatesToInject.next(autoupdate);
|
||||
} else if (this.disabledUntil !== null) {
|
||||
this._autoupdatesToInject.next(autoupdate);
|
||||
if (autoupdate.to_change_id >= this.disabledUntil) {
|
||||
this.disabledUntil = null;
|
||||
console.log('Throttling autoupdates again');
|
||||
}
|
||||
} else if (autoupdate.all_data) {
|
||||
// all_data=true (aka initial data) should be processed immediatly
|
||||
// but since there can be pending autoupdates, add it there and
|
||||
// process them now!
|
||||
this.pendingAutoupdates.push(autoupdate);
|
||||
this.processPendingAutoupdates();
|
||||
} else {
|
||||
this.pendingAutoupdates.push(autoupdate);
|
||||
this.receivedAutoupdate.emit();
|
||||
}
|
||||
}
|
||||
|
||||
public disableUntil(changeId: number): void {
|
||||
// Wait for an autoupdate with to_id >= changeId.
|
||||
if (!this.isActive) {
|
||||
return;
|
||||
}
|
||||
this.processPendingAutoupdates();
|
||||
// Checking with maxSeenChangeId is for the following race condition:
|
||||
// If the autoupdate comes before the response, it must not be throttled.
|
||||
// But flushing pending autoupdates is important since *if* the autoupdate
|
||||
// was early, it is in the pending queue.
|
||||
if (changeId <= this.maxSeenChangeId) {
|
||||
return;
|
||||
}
|
||||
console.log('Disable autoupdate until change id', changeId);
|
||||
this.disabledUntil = changeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* discard all pending autoupdates and resets the timer
|
||||
*/
|
||||
public discard(): void {
|
||||
this.pendingAutoupdates = [];
|
||||
}
|
||||
|
||||
private processPendingAutoupdates(): void {
|
||||
if (this.pendingAutoupdates.length === 0) {
|
||||
return;
|
||||
}
|
||||
const autoupdates = this.pendingAutoupdates;
|
||||
this.discard();
|
||||
|
||||
console.log(`Processing ${autoupdates.length} pending autoupdates`);
|
||||
const autoupdate = this.mergeAutoupdates(autoupdates);
|
||||
this._autoupdatesToInject.next(autoupdate);
|
||||
}
|
||||
|
||||
private mergeAutoupdates(autoupdates: AutoupdateFormat[]): AutoupdateFormat {
|
||||
const mergedAutoupdate: AutoupdateFormat = {
|
||||
changed: {},
|
||||
deleted: {},
|
||||
from_change_id: autoupdates[0].from_change_id,
|
||||
to_change_id: autoupdates[autoupdates.length - 1].to_change_id,
|
||||
all_data: false
|
||||
};
|
||||
|
||||
let lastToChangeId = null;
|
||||
for (const au of autoupdates) {
|
||||
if (lastToChangeId === null) {
|
||||
lastToChangeId = au.to_change_id;
|
||||
} else {
|
||||
if (au.from_change_id !== lastToChangeId) {
|
||||
console.warn('!!!', autoupdates, au);
|
||||
}
|
||||
lastToChangeId = au.to_change_id;
|
||||
}
|
||||
|
||||
this.applyAutoupdate(au, mergedAutoupdate);
|
||||
}
|
||||
|
||||
return mergedAutoupdate;
|
||||
}
|
||||
|
||||
private applyAutoupdate(from: AutoupdateFormat, into: AutoupdateFormat): void {
|
||||
if (from.all_data) {
|
||||
into.all_data = true;
|
||||
into.changed = from.changed;
|
||||
into.deleted = from.deleted;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const collection of Object.keys(from.deleted)) {
|
||||
for (const id of from.deleted[collection]) {
|
||||
if (into.changed[collection]) {
|
||||
into.changed[collection] = into.changed[collection].filter(obj => (obj as Identifiable).id !== id);
|
||||
}
|
||||
if (!into.deleted[collection]) {
|
||||
into.deleted[collection] = [];
|
||||
}
|
||||
into.deleted[collection].push(id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const collection of Object.keys(from.changed)) {
|
||||
for (const obj of from.changed[collection]) {
|
||||
if (into.deleted[collection]) {
|
||||
into.deleted[collection] = into.deleted[collection].filter(id => id !== (obj as Identifiable).id);
|
||||
}
|
||||
if (!into.changed[collection]) {
|
||||
into.changed[collection] = [];
|
||||
}
|
||||
into.changed[collection].push(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AutoupdateService } from './autoupdate.service';
|
||||
import { E2EImportsModule } from '../../../e2e-imports.module';
|
||||
|
||||
describe('AutoupdateService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [AutoupdateService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([AutoupdateService], (service: AutoupdateService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,178 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { AutoupdateFormat } from '../definitions/autoupdate-format';
|
||||
import { AutoupdateThrottleService } from './autoupdate-throttle.service';
|
||||
import { BaseModel } from '../../shared/models/base/base-model';
|
||||
import { CollectionStringMapperService } from './collection-string-mapper.service';
|
||||
import { CommunicationManagerService, OfflineError } from './communication-manager.service';
|
||||
import { DataStoreService, DataStoreUpdateManagerService } from './data-store.service';
|
||||
import { HttpService } from './http.service';
|
||||
import { Mutex } from '../promises/mutex';
|
||||
|
||||
/**
|
||||
* Handles the initial update and automatic updates
|
||||
* Incoming objects, usually BaseModels, will be saved in the dataStore (`this.DS`)
|
||||
* This service usually creates all models
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AutoupdateService {
|
||||
private mutex = new Mutex();
|
||||
|
||||
private streamCloseFn: () => void | null = null;
|
||||
|
||||
private lastMessageContainedAllData = false;
|
||||
|
||||
public constructor(
|
||||
private DS: DataStoreService,
|
||||
private modelMapper: CollectionStringMapperService,
|
||||
private DSUpdateManager: DataStoreUpdateManagerService,
|
||||
private communicationManager: CommunicationManagerService,
|
||||
private autoupdateThrottle: AutoupdateThrottleService
|
||||
) {
|
||||
this.communicationManager.startCommunicationEvent.subscribe(() => this.startAutoupdate());
|
||||
|
||||
this.autoupdateThrottle.autoupdatesToInject.subscribe(autoupdate => this.storeAutoupdate(autoupdate));
|
||||
}
|
||||
|
||||
public async startAutoupdate(changeId?: number): Promise<void> {
|
||||
this.stopAutoupdate();
|
||||
|
||||
try {
|
||||
this.streamCloseFn = await this.communicationManager.subscribe<AutoupdateFormat>(
|
||||
'/system/autoupdate',
|
||||
autoupdate => {
|
||||
this.autoupdateThrottle.newAutoupdate(autoupdate);
|
||||
},
|
||||
() => ({ change_id: (changeId ? changeId : this.DS.maxChangeId).toString() })
|
||||
);
|
||||
} catch (e) {
|
||||
if (!(e instanceof OfflineError)) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public stopAutoupdate(): void {
|
||||
if (this.streamCloseFn) {
|
||||
this.streamCloseFn();
|
||||
this.streamCloseFn = null;
|
||||
}
|
||||
this.autoupdateThrottle.discard();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the answer of incoming data, after it was throttled.
|
||||
*
|
||||
* Detects the Class of an incomming model, creates a new empty object and assigns
|
||||
* the data to it using the deserialize function. Also models that are flagged as deleted
|
||||
* will be removed from the data store.
|
||||
*
|
||||
* Handles the change ids of all autoupdates.
|
||||
*/
|
||||
private async storeAutoupdate(autoupdate: AutoupdateFormat): Promise<void> {
|
||||
const unlock = await this.mutex.lock();
|
||||
this.lastMessageContainedAllData = autoupdate.all_data;
|
||||
if (autoupdate.all_data) {
|
||||
await this.storeAllData(autoupdate);
|
||||
} else {
|
||||
await this.storePartialAutoupdate(autoupdate);
|
||||
}
|
||||
unlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores all data from the autoupdate. This means, that the DS is resetted and filled with just the
|
||||
* given data from the autoupdate.
|
||||
* @param autoupdate The autoupdate
|
||||
*/
|
||||
private async storeAllData(autoupdate: AutoupdateFormat): Promise<void> {
|
||||
let elements: BaseModel[] = [];
|
||||
Object.keys(autoupdate.changed).forEach(collection => {
|
||||
elements = elements.concat(this.mapObjectsToBaseModels(collection, autoupdate.changed[collection]));
|
||||
});
|
||||
|
||||
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS);
|
||||
await this.DS.set(elements, autoupdate.to_change_id);
|
||||
this.DSUpdateManager.commit(updateSlot, autoupdate.to_change_id, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* handles a normal autoupdate that is not a full update (all_data=false).
|
||||
* @param autoupdate The autoupdate
|
||||
*/
|
||||
private async storePartialAutoupdate(autoupdate: AutoupdateFormat): Promise<void> {
|
||||
const maxChangeId = this.DS.maxChangeId;
|
||||
|
||||
if (autoupdate.from_change_id <= maxChangeId && autoupdate.to_change_id <= maxChangeId) {
|
||||
console.log(`Ignore. Clients change id: ${maxChangeId}`);
|
||||
return; // Ignore autoupdates, that lay full behind our changeid.
|
||||
}
|
||||
|
||||
// Normal autoupdate
|
||||
if (autoupdate.from_change_id <= maxChangeId + 1 && autoupdate.to_change_id > maxChangeId) {
|
||||
await this.injectAutupdateIntoDS(autoupdate, true);
|
||||
} else {
|
||||
// autoupdate fully in the future. we are missing something!
|
||||
console.log('Autoupdate in the future', maxChangeId, autoupdate.from_change_id, autoupdate.to_change_id);
|
||||
this.startAutoupdate(); // restarts it.
|
||||
}
|
||||
}
|
||||
|
||||
private async injectAutupdateIntoDS(autoupdate: AutoupdateFormat, flush: boolean): Promise<void> {
|
||||
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS);
|
||||
|
||||
// Delete the removed objects from the DataStore
|
||||
for (const collection of Object.keys(autoupdate.deleted)) {
|
||||
await this.DS.remove(collection, autoupdate.deleted[collection]);
|
||||
}
|
||||
|
||||
// Add the objects to the DataStore.
|
||||
for (const collection of Object.keys(autoupdate.changed)) {
|
||||
await this.DS.add(this.mapObjectsToBaseModels(collection, autoupdate.changed[collection]));
|
||||
}
|
||||
|
||||
if (flush) {
|
||||
await this.DS.flushToStorage(autoupdate.to_change_id);
|
||||
}
|
||||
|
||||
this.DSUpdateManager.commit(updateSlot, autoupdate.to_change_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates baseModels for each plain object. If the collection is not registered,
|
||||
* A console error will be issued and an empty list returned.
|
||||
*
|
||||
* @param collection The collection all models have to be from.
|
||||
* @param models All models that should be mapped to BaseModels
|
||||
* @returns A list of basemodels constructed from the given models.
|
||||
*/
|
||||
private mapObjectsToBaseModels(collection: string, models: object[]): BaseModel[] {
|
||||
if (this.modelMapper.isCollectionRegistered(collection)) {
|
||||
const targetClass = this.modelMapper.getModelConstructor(collection);
|
||||
return models.map(model => new targetClass(model));
|
||||
} else {
|
||||
console.error(`Unregistered collection "${collection}". Ignore it.`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a full update: Requests all data from the server and sets the DS to the fresh data.
|
||||
*/
|
||||
public async doFullUpdate(): Promise<void> {
|
||||
if (this.lastMessageContainedAllData) {
|
||||
console.log('full update requested. Skipping, last message already contained all data');
|
||||
} else {
|
||||
console.log('requesting full update.');
|
||||
// The mutex is needed, so the DS is not cleared, if there is
|
||||
// another autoupdate running.
|
||||
const unlock = await this.mutex.lock();
|
||||
this.stopAutoupdate();
|
||||
await this.DS.clear();
|
||||
this.startAutoupdate();
|
||||
unlock();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CollectionStringMapperService } from './collection-string-mapper.service';
|
||||
import { E2EImportsModule } from '../../../e2e-imports.module';
|
||||
|
||||
describe('CollectionStringMapperService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [CollectionStringMapperService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([CollectionStringMapperService], (service: CollectionStringMapperService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,139 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { BaseRepository } from 'app/core/repositories/base-repository';
|
||||
import { BaseViewModel, TitleInformation, ViewModelConstructor } from 'app/site/base/base-view-model';
|
||||
import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model';
|
||||
|
||||
/**
|
||||
* Unifies the ModelConstructor and ViewModelConstructor.
|
||||
*/
|
||||
interface UnifiedConstructors {
|
||||
COLLECTIONSTRING: string;
|
||||
new (...args: any[]): any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every types supported: (View)ModelConstructors, repos and collectionstrings.
|
||||
*/
|
||||
type TypeIdentifier = UnifiedConstructors | BaseRepository<any, any, any> | string;
|
||||
|
||||
type CollectionStringMappedTypes = [
|
||||
ModelConstructor<BaseModel>,
|
||||
ViewModelConstructor<BaseViewModel>,
|
||||
BaseRepository<BaseViewModel<any>, BaseModel<any>, TitleInformation>
|
||||
];
|
||||
|
||||
/**
|
||||
* Registeres the mapping between collection strings, models constructors, view
|
||||
* model constructors and repositories.
|
||||
* All models need to be registered!
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CollectionStringMapperService {
|
||||
/**
|
||||
* Maps collection strings to mapping entries
|
||||
*/
|
||||
private collectionStringMapping: {
|
||||
[collectionString: string]: CollectionStringMappedTypes;
|
||||
} = {};
|
||||
|
||||
public constructor() {}
|
||||
|
||||
/**
|
||||
* Registers the combination of a collection string, model, view model and repository
|
||||
* @param collectionString
|
||||
* @param model
|
||||
*/
|
||||
public registerCollectionElement<V extends BaseViewModel<M>, M extends BaseModel>(
|
||||
model: ModelConstructor<M>,
|
||||
viewModel: ViewModelConstructor<V>,
|
||||
repository: BaseRepository<V, M, TitleInformation>
|
||||
): void {
|
||||
this.collectionStringMapping[model.COLLECTIONSTRING] = [model, viewModel, repository];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param obj The object to get the collection string from.
|
||||
* @returns the collectionstring
|
||||
*/
|
||||
public getCollectionString(obj: TypeIdentifier): string {
|
||||
if (typeof obj === 'string') {
|
||||
return obj;
|
||||
} else {
|
||||
return obj.COLLECTIONSTRING;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true, if the given collection is known by this service.
|
||||
*/
|
||||
public isCollectionRegistered(collectionString: string): boolean {
|
||||
return !!this.collectionStringMapping[collectionString];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param obj The object to get the model constructor from.
|
||||
* @returns the model constructor
|
||||
*/
|
||||
public getModelConstructor<M extends BaseModel>(obj: TypeIdentifier): ModelConstructor<M> | null {
|
||||
if (this.isCollectionRegistered(this.getCollectionString(obj))) {
|
||||
return this.collectionStringMapping[this.getCollectionString(obj)][0] as ModelConstructor<M>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param obj The object to get the view model constructor from.
|
||||
* @returns the view model constructor
|
||||
*/
|
||||
public getViewModelConstructor<M extends BaseViewModel>(obj: TypeIdentifier): ViewModelConstructor<M> | null {
|
||||
if (this.isCollectionRegistered(this.getCollectionString(obj))) {
|
||||
return this.collectionStringMapping[this.getCollectionString(obj)][1] as ViewModelConstructor<M>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param obj The object to get the repository from.
|
||||
* @returns the repository
|
||||
*/
|
||||
public getRepository<V extends BaseViewModel, M extends BaseModel, T extends TitleInformation>(
|
||||
obj: TypeIdentifier
|
||||
): BaseRepository<V & T, M, T> | null {
|
||||
if (this.isCollectionRegistered(this.getCollectionString(obj))) {
|
||||
return this.collectionStringMapping[this.getCollectionString(obj)][2] as BaseRepository<V & T, M, T>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns all registered repositories.
|
||||
*/
|
||||
public getAllRepositories(): BaseRepository<any, any, any>[] {
|
||||
return Object.values(this.collectionStringMapping).map((types: CollectionStringMappedTypes) => types[2]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given element id. It must have the form `<collection>:<id>`, with
|
||||
* <collection> being a registered collection and the id a valid integer greater then 0.
|
||||
*
|
||||
* @param elementId The element id.
|
||||
* @returns true, if the element id is valid.
|
||||
*/
|
||||
public isElementIdValid(elementId: any): boolean {
|
||||
if (!elementId || typeof elementId !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const splitted = elementId.split(':');
|
||||
if (splitted.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const id = parseInt(splitted[1], 10);
|
||||
if (isNaN(id) || id <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Object.keys(this.collectionStringMapping).some(collection => collection === splitted[0]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
import { HttpParams } from '@angular/common/http';
|
||||
import { EventEmitter, Injectable } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { HttpService } from './http.service';
|
||||
import { OfflineBroadcastService, OfflineReason } from './offline-broadcast.service';
|
||||
import { OperatorService } from './operator.service';
|
||||
import { SleepPromise } from '../promises/sleep';
|
||||
import {
|
||||
CommunicationError,
|
||||
ErrorType,
|
||||
Stream,
|
||||
StreamContainer,
|
||||
StreamingCommunicationService,
|
||||
verboseErrorType
|
||||
} from './streaming-communication.service';
|
||||
|
||||
type HttpParamsGetter = () => HttpParams | { [param: string]: string | string[] };
|
||||
|
||||
const MAX_STREAM_FAILURE_RETRIES = 3;
|
||||
|
||||
export class OfflineError extends Error {
|
||||
public constructor() {
|
||||
super('');
|
||||
this.name = 'OfflineError';
|
||||
}
|
||||
}
|
||||
|
||||
interface StreamConnectionWrapper {
|
||||
id: number;
|
||||
url: string;
|
||||
messageHandler: (message: any) => void;
|
||||
params: HttpParamsGetter;
|
||||
stream?: Stream<any>;
|
||||
hasErroredAmount: number;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CommunicationManagerService {
|
||||
private communicationAllowed = false;
|
||||
|
||||
private readonly _startCommunicationEvent = new EventEmitter<void>();
|
||||
|
||||
public get startCommunicationEvent(): Observable<void> {
|
||||
return this._startCommunicationEvent.asObservable();
|
||||
}
|
||||
|
||||
private readonly _stopCommunicationEvent = new EventEmitter<void>();
|
||||
|
||||
public get stopCommunicationEvent(): Observable<void> {
|
||||
return this._stopCommunicationEvent.asObservable();
|
||||
}
|
||||
|
||||
private streamContainers: { [id: number]: StreamContainer<any> } = {};
|
||||
|
||||
public constructor(
|
||||
private streamingCommunicationService: StreamingCommunicationService,
|
||||
private offlineBroadcastService: OfflineBroadcastService,
|
||||
private http: HttpService,
|
||||
private operatorService: OperatorService
|
||||
) {
|
||||
this.offlineBroadcastService.goOfflineObservable.subscribe(() => this.closeConnections());
|
||||
}
|
||||
|
||||
public async subscribe<T>(
|
||||
url: string,
|
||||
messageHandler: (message: T) => void,
|
||||
params?: HttpParamsGetter
|
||||
): Promise<() => void> {
|
||||
if (!params) {
|
||||
params = () => null;
|
||||
}
|
||||
|
||||
const streamContainer = new StreamContainer(url, messageHandler, params);
|
||||
return await this.connectWithWrapper(streamContainer);
|
||||
}
|
||||
|
||||
public startCommunication(): void {
|
||||
if (this.communicationAllowed) {
|
||||
console.error('Illegal state! Do not emit this event multiple times');
|
||||
} else {
|
||||
this.communicationAllowed = true;
|
||||
this._startCommunicationEvent.emit();
|
||||
}
|
||||
}
|
||||
|
||||
private async connectWithWrapper<T>(streamContainer: StreamContainer<T>): Promise<() => void> {
|
||||
console.log('connect', streamContainer, streamContainer.stream);
|
||||
const errorHandler = (type: ErrorType, error: CommunicationError, message: string) =>
|
||||
this.handleError(streamContainer, type, error, message);
|
||||
this.streamingCommunicationService.subscribe(streamContainer, errorHandler);
|
||||
this.streamContainers[streamContainer.id] = streamContainer;
|
||||
return () => this.close(streamContainer);
|
||||
}
|
||||
|
||||
private async handleError<T>(
|
||||
streamContainer: StreamContainer<T>,
|
||||
type: ErrorType,
|
||||
error: CommunicationError,
|
||||
message: string
|
||||
): Promise<void> {
|
||||
console.log('handle Error', streamContainer, streamContainer.stream, verboseErrorType(type), error, message);
|
||||
streamContainer.stream.close();
|
||||
streamContainer.stream = null;
|
||||
|
||||
streamContainer.hasErroredAmount++;
|
||||
if (streamContainer.hasErroredAmount > MAX_STREAM_FAILURE_RETRIES) {
|
||||
this.goOffline(streamContainer, OfflineReason.ConnectionLost);
|
||||
} else if (type === ErrorType.Client && error.type === 'auth_required') {
|
||||
this.goOffline(streamContainer, OfflineReason.WhoAmIFailed);
|
||||
} else {
|
||||
// retry it after some time:
|
||||
console.log(
|
||||
`Retry no. ${streamContainer.hasErroredAmount} of ${MAX_STREAM_FAILURE_RETRIES} for ${streamContainer.url}`
|
||||
);
|
||||
try {
|
||||
await this.delayAndCheckReconnection(streamContainer);
|
||||
await this.connectWithWrapper(streamContainer);
|
||||
} catch (e) {
|
||||
// delayAndCheckReconnection can throw an OfflineError,
|
||||
// which are just an 'abord mission' signal. Here, those errors can be ignored.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async delayAndCheckReconnection<T>(streamContainer: StreamContainer<T>): Promise<void> {
|
||||
let delay;
|
||||
if (streamContainer.hasErroredAmount === 1) {
|
||||
delay = 500; // the first error has a small delay since these error can happen normally.
|
||||
} else {
|
||||
delay = Math.floor(Math.random() * 3000 + 2000);
|
||||
}
|
||||
console.log(`retry again in ${delay} ms`);
|
||||
|
||||
await SleepPromise(delay);
|
||||
|
||||
// do not continue, if we are offline!
|
||||
if (this.offlineBroadcastService.isOffline()) {
|
||||
console.log('we are offline?');
|
||||
throw new OfflineError();
|
||||
}
|
||||
|
||||
// do not continue, if we are offline!
|
||||
if (!this.shouldRetryConnecting()) {
|
||||
console.log('operator changed, do not rety');
|
||||
throw new OfflineError(); // TODO: This error is not really good....
|
||||
}
|
||||
}
|
||||
|
||||
public closeConnections(): void {
|
||||
for (const streamWrapper of Object.values(this.streamContainers)) {
|
||||
if (streamWrapper.stream) {
|
||||
streamWrapper.stream.close();
|
||||
}
|
||||
}
|
||||
this.streamContainers = {};
|
||||
this.communicationAllowed = false;
|
||||
this._stopCommunicationEvent.emit();
|
||||
}
|
||||
|
||||
private goOffline<T>(streamContainer: StreamContainer<T>, reason: OfflineReason): void {
|
||||
delete this.streamContainers[streamContainer.id];
|
||||
this.closeConnections(); // here we close the connections early.
|
||||
this.offlineBroadcastService.goOffline(reason);
|
||||
}
|
||||
|
||||
private close(streamConnectionWrapper: StreamConnectionWrapper): void {
|
||||
if (this.streamContainers[streamConnectionWrapper.id]) {
|
||||
this.streamContainers[streamConnectionWrapper.id].stream.close();
|
||||
delete this.streamContainers[streamConnectionWrapper.id];
|
||||
}
|
||||
}
|
||||
|
||||
// Checks the operator: If we do not have a valid user,
|
||||
// do not even try to connect again..
|
||||
private shouldRetryConnecting(): boolean {
|
||||
return this.operatorService.guestsEnabled || !!this.operatorService.user;
|
||||
}
|
||||
|
||||
public async isCommunicationServiceOnline(): Promise<boolean> {
|
||||
try {
|
||||
const response = await this.http.get<{ healthy: boolean }>('/system/health');
|
||||
return !!response.healthy;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ConstantsService } from './constants.service';
|
||||
import { E2EImportsModule } from '../../../e2e-imports.module';
|
||||
|
||||
describe('ConstantsService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [ConstantsService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([ConstantsService], (service: ConstantsService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { environment } from 'environments/environment';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { filter } from 'rxjs/operators';
|
||||
|
||||
import { CommunicationManagerService } from './communication-manager.service';
|
||||
import { HttpService } from './http.service';
|
||||
|
||||
/**
|
||||
* constants have a key associated with the data.
|
||||
*/
|
||||
interface Constants {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get constants from the server.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* this.constantsService.get('Settings').subscribe(constant => {
|
||||
* console.log(constant);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ConstantsService {
|
||||
/**
|
||||
* The constants
|
||||
*/
|
||||
private constants: Constants = {};
|
||||
|
||||
/**
|
||||
* Pending requests will be notified by these subjects, one per key.
|
||||
*/
|
||||
private subjects: { [key: string]: BehaviorSubject<any> } = {};
|
||||
|
||||
public constructor(communicationManager: CommunicationManagerService, private http: HttpService) {
|
||||
communicationManager.startCommunicationEvent.subscribe(async () => {
|
||||
this.constants = await this.http.get<Constants>(environment.urlPrefix + '/core/constants/');
|
||||
Object.keys(this.subjects).forEach(key => {
|
||||
this.subjects[key].next(this.constants[key]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the constant named by key.
|
||||
* @param key The constant to get.
|
||||
*/
|
||||
public get<T>(key: string): Observable<T> {
|
||||
if (!this.subjects[key]) {
|
||||
this.subjects[key] = new BehaviorSubject<any>(this.constants[key]);
|
||||
}
|
||||
return this.subjects[key].asObservable().pipe(filter(x => !!x));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DataSendService } from './data-send.service';
|
||||
import { E2EImportsModule } from '../../../e2e-imports.module';
|
||||
|
||||
describe('DataSendService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [DataSendService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([DataSendService], (service: DataSendService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { BaseModel } from '../../shared/models/base/base-model';
|
||||
import { HttpService } from './http.service';
|
||||
import { Identifiable } from '../../shared/models/base/identifiable';
|
||||
|
||||
/**
|
||||
* Send data back to server. Cares about the right REST routes.
|
||||
*
|
||||
* Contrast to dataStore service
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DataSendService {
|
||||
/**
|
||||
* Construct a DataSendService
|
||||
*
|
||||
* @param httpService The HTTP Service
|
||||
*/
|
||||
public constructor(private httpService: HttpService) {}
|
||||
|
||||
/**
|
||||
* Sends a post request with the model to the server to create it.
|
||||
*
|
||||
* @param model The model to create.
|
||||
*/
|
||||
public async createModel(model: BaseModel): Promise<Identifiable> {
|
||||
const restPath = `/rest/${model.collectionString}/`;
|
||||
return await this.httpService.post<Identifiable>(restPath, model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to fully update a model on the server.
|
||||
*
|
||||
* @param model The model that is meant to be changed.
|
||||
*/
|
||||
public async updateModel(model: BaseModel): Promise<void> {
|
||||
const restPath = `/rest/${model.collectionString}/${model.id}/`;
|
||||
await this.httpService.put(restPath, model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a model partially on the server.
|
||||
*
|
||||
* @param model The model to partially update.
|
||||
*/
|
||||
public async partialUpdateModel(model: BaseModel): Promise<void> {
|
||||
const restPath = `/rest/${model.collectionString}/${model.id}/`;
|
||||
await this.httpService.patch(restPath, model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given model on the server.
|
||||
*
|
||||
* @param model the model that shall be deleted.
|
||||
*/
|
||||
public async deleteModel(model: BaseModel): Promise<void> {
|
||||
const restPath = `/rest/${model.collectionString}/${model.id}/`;
|
||||
await this.httpService.delete(restPath);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DataStoreUpgradeService } from './data-store-upgrade.service';
|
||||
import { E2EImportsModule } from '../../../e2e-imports.module';
|
||||
|
||||
describe('DataStoreUpgradeService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [DataStoreUpgradeService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([DataStoreUpgradeService], (service: DataStoreUpgradeService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,91 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { AutoupdateService } from './autoupdate.service';
|
||||
import { ConstantsService } from './constants.service';
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
interface SchemaVersion {
|
||||
db: string;
|
||||
config: number;
|
||||
migration: number;
|
||||
}
|
||||
|
||||
function isSchemaVersion(obj: any): obj is SchemaVersion {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return false;
|
||||
}
|
||||
return obj.db !== undefined && obj.config !== undefined && obj.migration !== undefined;
|
||||
}
|
||||
|
||||
const SCHEMA_VERSION = 'SchemaVersion';
|
||||
|
||||
/**
|
||||
* Manages upgrading the DataStore, if the migration version from the server is higher than the current one.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DataStoreUpgradeService {
|
||||
/**
|
||||
* Notify, when upgrade has checked.
|
||||
*/
|
||||
public readonly upgradeChecked = new BehaviorSubject(false);
|
||||
|
||||
/**
|
||||
* @param autoupdateService
|
||||
* @param constantsService
|
||||
* @param storageService
|
||||
*/
|
||||
public constructor(
|
||||
private autoupdateService: AutoupdateService,
|
||||
private constantsService: ConstantsService,
|
||||
private storageService: StorageService
|
||||
) {
|
||||
// Prevent the schema version to be cleard. This is important
|
||||
// after a reset from OpenSlides, because the complete data is
|
||||
// queried from the server and we do not want also to trigger a reload
|
||||
// by changing the schema from null -> <schema>.
|
||||
this.storageService.addNoClearKey(SCHEMA_VERSION);
|
||||
|
||||
this.constantsService
|
||||
.get<SchemaVersion>(SCHEMA_VERSION)
|
||||
.subscribe(serverVersion => this.checkForUpgrade(serverVersion));
|
||||
}
|
||||
|
||||
public async checkForUpgrade(serverVersion: SchemaVersion): Promise<boolean> {
|
||||
this.upgradeChecked.next(false);
|
||||
console.log('Server schema version:', serverVersion);
|
||||
const clientVersion = await this.storageService.get<SchemaVersion>(SCHEMA_VERSION);
|
||||
await this.storageService.set(SCHEMA_VERSION, serverVersion);
|
||||
|
||||
let doUpgrade = false;
|
||||
if (isSchemaVersion(clientVersion)) {
|
||||
if (clientVersion.db !== serverVersion.db) {
|
||||
console.log(`\tDB id changed from ${clientVersion.db} to ${serverVersion.db}`);
|
||||
doUpgrade = true;
|
||||
}
|
||||
if (clientVersion.config !== serverVersion.config) {
|
||||
console.log(`\tConfig changed from ${clientVersion.config} to ${serverVersion.config}`);
|
||||
doUpgrade = true;
|
||||
}
|
||||
if (clientVersion.migration !== serverVersion.migration) {
|
||||
console.log(`\tMigration changed from ${clientVersion.migration} to ${serverVersion.migration}`);
|
||||
doUpgrade = true;
|
||||
}
|
||||
} else {
|
||||
console.log('\tNo client schema version.');
|
||||
doUpgrade = true;
|
||||
}
|
||||
|
||||
if (doUpgrade) {
|
||||
console.log('\t-> In result of a schema version change: Do full update.');
|
||||
await this.autoupdateService.doFullUpdate();
|
||||
} else {
|
||||
console.log('\t-> No upgrade needed.');
|
||||
}
|
||||
this.upgradeChecked.next(true);
|
||||
return doUpgrade;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DataStoreService } from './data-store.service';
|
||||
|
||||
describe('DS', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [DataStoreService]
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,684 @@
|
|||
import { EventEmitter, Injectable } from '@angular/core';
|
||||
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
|
||||
import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model';
|
||||
import { CollectionStringMapperService } from './collection-string-mapper.service';
|
||||
import { Deferred } from '../promises/deferred';
|
||||
import { OpenSlidesStatusService } from './openslides-status.service';
|
||||
import { RelationCacheService } from './relation-cache.service';
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
/**
|
||||
* Represents information about a deleted model.
|
||||
*
|
||||
* As the model doesn't exist anymore, just the former id and collection is known.
|
||||
*/
|
||||
export interface DeletedInformation {
|
||||
collection: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface CollectionIds {
|
||||
[collection: string]: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for collecting data during the update phase of the DataStore.
|
||||
*/
|
||||
export class UpdateSlot {
|
||||
/**
|
||||
* Count instnaces of this class to easily compare them.
|
||||
*/
|
||||
private static ID_COUTNER = 1;
|
||||
|
||||
/**
|
||||
* Mapping of changed model ids to their collection.
|
||||
*/
|
||||
private changedModels: CollectionIds = {};
|
||||
|
||||
/**
|
||||
* Mapping of deleted models to their collection.
|
||||
*/
|
||||
private deletedModels: CollectionIds = {};
|
||||
|
||||
/**
|
||||
* The object's id.
|
||||
*/
|
||||
private _id: number;
|
||||
|
||||
/**
|
||||
* @param DS Carries the DataStore: TODO (see below `DataStoreUpdateManagerService.getNewUpdateSlot`)
|
||||
*/
|
||||
public constructor(public readonly DS: DataStoreService) {
|
||||
this._id = UpdateSlot.ID_COUTNER++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds changed model information
|
||||
*
|
||||
* @param collection The collection
|
||||
* @param id The id
|
||||
*/
|
||||
public addChangedModel(collection: string, id: number): void {
|
||||
if (!this.changedModels[collection]) {
|
||||
this.changedModels[collection] = [];
|
||||
}
|
||||
this.changedModels[collection].push(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds deleted model information
|
||||
*
|
||||
* @param collection The collection
|
||||
* @param id The id
|
||||
*/
|
||||
public addDeletedModel(collection: string, id: number): void {
|
||||
if (!this.deletedModels[collection]) {
|
||||
this.deletedModels[collection] = [];
|
||||
}
|
||||
this.deletedModels[collection].push(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param collection The collection
|
||||
* @returns the list of changed model ids for the collection
|
||||
*/
|
||||
public getChangedModelIdsForCollection(collection: string): number[] {
|
||||
return this.changedModels[collection] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the mapping of all changed models
|
||||
*/
|
||||
public getChangedModels(): CollectionIds {
|
||||
return this.changedModels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param collection The collection
|
||||
* @returns the list of deleted model ids for the collection
|
||||
*/
|
||||
public getDeletedModelIdsForCollection(collection: string): number[] {
|
||||
return this.deletedModels[collection] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the mapping of all deleted models
|
||||
*/
|
||||
public getDeletedModels(): CollectionIds {
|
||||
return this.deletedModels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns all changed and deleted model ids in one array. If an id was
|
||||
* changed and deleted, it will be there twice! But this should not be the case.
|
||||
*/
|
||||
public getAllModelsIdsForCollection(collection: string): number[] {
|
||||
return this.getDeletedModelIdsForCollection(collection).concat(
|
||||
this.getChangedModelIdsForCollection(collection)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this object to another update slot.
|
||||
*/
|
||||
public equal(other: UpdateSlot): boolean {
|
||||
return this._id === other._id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* represents a collection on the Django server, uses an ID to access a {@link BaseModel}.
|
||||
*
|
||||
* Part of {@link DataStoreService}
|
||||
*/
|
||||
interface ModelCollection {
|
||||
[id: number]: BaseModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a serialized collection.
|
||||
*/
|
||||
interface JsonCollection {
|
||||
[id: number]: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The actual storage that stores collections, accessible by strings.
|
||||
*
|
||||
* {@link DataStoreService}
|
||||
*/
|
||||
interface ModelStorage {
|
||||
[collectionString: string]: ModelCollection;
|
||||
}
|
||||
|
||||
/**
|
||||
* A storage of serialized collection elements.
|
||||
*/
|
||||
interface JsonStorage {
|
||||
[collectionString: string]: JsonCollection;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Avoid circular dependencies between `DataStoreUpdateManagerService` and
|
||||
* `DataStoreService` and split them into two files
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DataStoreUpdateManagerService {
|
||||
/**
|
||||
* The current update slot
|
||||
*/
|
||||
private currentUpdateSlot: UpdateSlot | null = null;
|
||||
|
||||
/**
|
||||
* Requests for getting an update slot.
|
||||
*/
|
||||
private updateSlotRequests: Deferred[] = [];
|
||||
|
||||
/**
|
||||
* @param mapperService
|
||||
*/
|
||||
public constructor(
|
||||
private mapperService: CollectionStringMapperService,
|
||||
private relationCacheService: RelationCacheService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the current update slot.
|
||||
*/
|
||||
public getCurrentUpdateSlot(): UpdateSlot | null {
|
||||
return this.currentUpdateSlot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new update slot. Returns a promise that must be awaited, if there is another
|
||||
* update in progress.
|
||||
*
|
||||
* @param DS The DataStore. This is a hack, becuase we cannot use the DataStore
|
||||
* here, because these are cyclic dependencies... --> TODO
|
||||
*/
|
||||
public async getNewUpdateSlot(DS: DataStoreService): Promise<UpdateSlot> {
|
||||
if (this.currentUpdateSlot) {
|
||||
const request = new Deferred();
|
||||
this.updateSlotRequests.push(request);
|
||||
await request;
|
||||
}
|
||||
this.currentUpdateSlot = new UpdateSlot(DS);
|
||||
return this.currentUpdateSlot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the given update slot. This slot must be the current one. If there are requests
|
||||
* for update slots queued, the next one will be served.
|
||||
*
|
||||
* Note: I added this param to make sure, that only the user of the slot
|
||||
* can commit the update and no one else.
|
||||
*
|
||||
* @param slot The slot to commit
|
||||
*/
|
||||
public commit(slot: UpdateSlot, changeId: number, resetCache: boolean = false): void {
|
||||
if (!this.currentUpdateSlot || !this.currentUpdateSlot.equal(slot)) {
|
||||
throw new Error('No or wrong update slot to be finished!');
|
||||
}
|
||||
this.currentUpdateSlot = null;
|
||||
|
||||
// notify repositories in two phases
|
||||
const repositories = this.mapperService.getAllRepositories();
|
||||
|
||||
if (resetCache) {
|
||||
this.relationCacheService.reset();
|
||||
}
|
||||
|
||||
// Phase 1: deleting and creating of view models (in this order)
|
||||
repositories.forEach(repo => {
|
||||
const deletedModelIds = slot.getDeletedModelIdsForCollection(repo.collectionString);
|
||||
repo.deleteModels(deletedModelIds);
|
||||
this.relationCacheService.registerDeletedModels(repo.collectionString, deletedModelIds);
|
||||
const changedModelIds = slot.getChangedModelIdsForCollection(repo.collectionString);
|
||||
repo.changedModels(changedModelIds);
|
||||
this.relationCacheService.registerChangedModels(repo.collectionString, changedModelIds, changeId);
|
||||
});
|
||||
|
||||
// Phase 2: updating all repositories
|
||||
repositories.forEach(repo => {
|
||||
repo.commitUpdate(slot.getAllModelsIdsForCollection(repo.collectionString));
|
||||
});
|
||||
|
||||
slot.DS.triggerModifiedObservable();
|
||||
|
||||
this.serveNextSlot();
|
||||
}
|
||||
|
||||
public dropUpdateSlot(): void {
|
||||
this.currentUpdateSlot = null;
|
||||
this.serveNextSlot();
|
||||
}
|
||||
|
||||
private serveNextSlot(): void {
|
||||
if (this.updateSlotRequests.length > 0) {
|
||||
console.log('Concurrent update slots');
|
||||
const request = this.updateSlotRequests.pop();
|
||||
request.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* All mighty DataStore that comes with all OpenSlides components.
|
||||
* Use this.DS in an OpenSlides Component to Access the store.
|
||||
* Used by a lot of components, classes and services.
|
||||
* Changes can be observed
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DataStoreService {
|
||||
private static cachePrefix = 'DS:';
|
||||
|
||||
/** We will store the data twice: One as instances of the actual models in the _store
|
||||
* and one serialized version in the _serializedStore for the cache. Both should be updated in
|
||||
* all cases equal!
|
||||
*/
|
||||
private modelStore: ModelStorage = {};
|
||||
private jsonStore: JsonStorage = {};
|
||||
|
||||
/**
|
||||
* Subjects for changed elements (notified, even if there is a current update slot) for
|
||||
* a specific collection.
|
||||
*/
|
||||
private changedSubjects: { [collection: string]: Subject<BaseModel> } = {};
|
||||
|
||||
/**
|
||||
* Observable subject for changed or deleted models in the datastore.
|
||||
*/
|
||||
private readonly modifiedSubject: Subject<void> = new Subject<void>();
|
||||
|
||||
/**
|
||||
* Observe the datastore for changes and deletions.
|
||||
*
|
||||
* @return an observable for changed and deleted objects.
|
||||
*/
|
||||
public get modifiedObservable(): Observable<void> {
|
||||
return this.modifiedSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Observable subject for changed or deleted models in the datastore.
|
||||
*/
|
||||
private readonly clearEvent: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
/**
|
||||
* Observe the datastore for changes and deletions.
|
||||
*
|
||||
* @return an observable for changed and deleted objects.
|
||||
*/
|
||||
public get clearObservable(): Observable<void> {
|
||||
return this.clearEvent.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximal change id from this DataStore.
|
||||
*/
|
||||
private _maxChangeId = 0;
|
||||
|
||||
/**
|
||||
* returns the maxChangeId of the DataStore.
|
||||
*/
|
||||
public get maxChangeId(): number {
|
||||
return this._maxChangeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param storageService use StorageService to preserve the DataStore.
|
||||
* @param modelMapper
|
||||
* @param DSUpdateManager
|
||||
*/
|
||||
public constructor(
|
||||
private storageService: StorageService,
|
||||
private modelMapper: CollectionStringMapperService,
|
||||
private DSUpdateManager: DataStoreUpdateManagerService,
|
||||
private statusService: OpenSlidesStatusService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get an model observable for models from a given collection. These observable will be notified,
|
||||
* even if there is an active update slot. So use this with caution (-> only collections with less models).
|
||||
*
|
||||
* @param collectionType The collection
|
||||
*/
|
||||
public getChangeObservable<T extends BaseModel>(collectionType: ModelConstructor<T> | string): Observable<T> {
|
||||
const collection = this.getCollectionString(collectionType);
|
||||
if (!this.changedSubjects[collection]) {
|
||||
this.changedSubjects[collection] = new Subject();
|
||||
}
|
||||
return this.changedSubjects[collection].asObservable() as Observable<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the DataStore from cache and instantiate all models out of the serialized version.
|
||||
* If something fails, the DS is cleared, so fresh data can be requrested from the server.
|
||||
*
|
||||
* @returns The max change id.
|
||||
*/
|
||||
public async initFromStorage(): Promise<void> {
|
||||
// This promise will be resolved with cached datastore.
|
||||
const store = await this.storageService.get<JsonStorage>(DataStoreService.cachePrefix + 'DS');
|
||||
if (!store) {
|
||||
await this.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this);
|
||||
|
||||
try {
|
||||
// There is a store. Deserialize it
|
||||
this.jsonStore = store;
|
||||
this.modelStore = this.deserializeJsonStore(this.jsonStore);
|
||||
|
||||
// Get the maxChangeId from the cache
|
||||
let maxChangeId = await this.storageService.get<number>(DataStoreService.cachePrefix + 'maxChangeId');
|
||||
if (!maxChangeId) {
|
||||
maxChangeId = 0;
|
||||
}
|
||||
this._maxChangeId = maxChangeId;
|
||||
|
||||
// update observers
|
||||
Object.keys(this.modelStore).forEach(collection => {
|
||||
Object.keys(this.modelStore[collection]).forEach(id => {
|
||||
this.publishChangedInformation(this.modelStore[collection][id]);
|
||||
});
|
||||
});
|
||||
|
||||
this.DSUpdateManager.commit(updateSlot, maxChangeId, true);
|
||||
} catch (e) {
|
||||
this.DSUpdateManager.dropUpdateSlot();
|
||||
await this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialze the given serializedStorage and returns a Storage.
|
||||
* @param serializedStore The store to deserialize
|
||||
* @returns The serialized storage
|
||||
*/
|
||||
private deserializeJsonStore(serializedStore: JsonStorage): ModelStorage {
|
||||
const storage: ModelStorage = {};
|
||||
Object.keys(serializedStore).forEach(collectionString => {
|
||||
storage[collectionString] = {} as ModelCollection;
|
||||
const target = this.modelMapper.getModelConstructor(collectionString);
|
||||
if (target) {
|
||||
Object.keys(serializedStore[collectionString]).forEach(id => {
|
||||
const data = JSON.parse(serializedStore[collectionString][id]);
|
||||
storage[collectionString][id] = new target(data);
|
||||
});
|
||||
}
|
||||
});
|
||||
return storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the complete DataStore and Cache.
|
||||
*/
|
||||
public async clear(): Promise<void> {
|
||||
this.modelStore = {};
|
||||
this.jsonStore = {};
|
||||
this._maxChangeId = 0;
|
||||
await this.storageService.remove(DataStoreService.cachePrefix + 'DS');
|
||||
await this.storageService.remove(DataStoreService.cachePrefix + 'maxChangeId');
|
||||
this.clearEvent.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the collection _string_ based on the model given. If a string is given, it's just returned.
|
||||
* @param collectionType Either a Model constructor or a string.
|
||||
* @returns the collection string
|
||||
*/
|
||||
private getCollectionString<T extends BaseModel<T>>(collectionType: ModelConstructor<T> | string): string {
|
||||
if (typeof collectionType === 'string') {
|
||||
return collectionType;
|
||||
} else {
|
||||
return this.modelMapper.getCollectionString(collectionType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read one model based on the collection and id from the DataStore.
|
||||
*
|
||||
* @param collectionType The desired BaseModel or collectionString to be read from the dataStore
|
||||
* @param ids One ID of the BaseModel
|
||||
* @return The given BaseModel-subclass instance
|
||||
* @example: this.DS.get(User, 1)
|
||||
* @example: this.DS.get<Countdown>('core/countdown', 2)
|
||||
*/
|
||||
public get<T extends BaseModel<T>>(collectionType: ModelConstructor<T> | string, id: number): T {
|
||||
const collectionString = this.getCollectionString<T>(collectionType);
|
||||
|
||||
const collection: ModelCollection = this.modelStore[collectionString];
|
||||
if (!collection) {
|
||||
return;
|
||||
} else {
|
||||
return collection[id] as T;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read multiple ID's from dataStore.
|
||||
*
|
||||
* @param collectionType The desired BaseModel or collectionString to be read from the dataStore
|
||||
* @param ids Multiple IDs as a list of IDs of BaseModel
|
||||
* @return The BaseModel-list corresponding to the given ID(s)
|
||||
* @example: this.DS.getMany(User, [1,2,3,4,5])
|
||||
* @example: this.DS.getMany<User>('users/user', [1,2,3,4,5])
|
||||
*/
|
||||
public getMany<T extends BaseModel<T>>(collectionType: ModelConstructor<T> | string, ids: number[]): T[] {
|
||||
const collectionString = this.getCollectionString<T>(collectionType);
|
||||
|
||||
const collection: ModelCollection = this.modelStore[collectionString];
|
||||
if (!collection) {
|
||||
return [];
|
||||
}
|
||||
const models = ids
|
||||
.map(id => {
|
||||
return collection[id];
|
||||
})
|
||||
.filter(model => !!model); // remove non valid models.
|
||||
return models as T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all models of the given collection from the DataStore.
|
||||
*
|
||||
* @param collectionType The desired BaseModel or collectionString to be read from the dataStore
|
||||
* @return The BaseModel-list of all instances of T
|
||||
* @example: this.DS.getAll(User)
|
||||
* @example: this.DS.getAll<User>('users/user')
|
||||
*/
|
||||
public getAll<T extends BaseModel<T>>(collectionType: ModelConstructor<T> | string): T[] {
|
||||
const collectionString = this.getCollectionString<T>(collectionType);
|
||||
|
||||
const collection: ModelCollection = this.modelStore[collectionString];
|
||||
if (!collection) {
|
||||
return [];
|
||||
} else {
|
||||
return Object.values(collection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the dataStore by type.
|
||||
*
|
||||
* @param collectionType The desired BaseModel type to be read from the dataStore
|
||||
* @param callback a filter function
|
||||
* @return The BaseModel-list corresponding to the filter function
|
||||
* @example this.DS.filter<User>(User, myUser => myUser.first_name === "Max")
|
||||
*/
|
||||
public filter<T extends BaseModel<T>>(
|
||||
collectionType: ModelConstructor<T> | string,
|
||||
callback: (model: T) => boolean
|
||||
): T[] {
|
||||
return this.getAll<T>(collectionType).filter(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a model item in the dataStore by type.
|
||||
*
|
||||
* @param collectionType The desired BaseModel type to be read from the dataStore
|
||||
* @param callback a find function
|
||||
* @return The first BaseModel item matching the filter function
|
||||
* @example this.DS.find<User>(User, myUser => myUser.first_name === "Jenny")
|
||||
*/
|
||||
public find<T extends BaseModel<T>>(
|
||||
collectionType: ModelConstructor<T> | string,
|
||||
callback: (model: T) => boolean
|
||||
): T {
|
||||
return this.getAll<T>(collectionType).find(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one or multiple models to dataStore.
|
||||
*
|
||||
* @param models BaseModels to add to the store
|
||||
* @param changeId The changeId of this update. If given, the storage will be flushed to the
|
||||
* cache. Else one can call {@method flushToStorage} to do this manually.
|
||||
* @example this.DS.add([new User(1)])
|
||||
* @example this.DS.add([new User(2), new User(3)])
|
||||
* @example this.DS.add(arrayWithUsers, changeId)
|
||||
*/
|
||||
public async add(models: BaseModel[], changeId?: number): Promise<void> {
|
||||
models.forEach(model => {
|
||||
const collection = model.collectionString;
|
||||
if (this.modelStore[collection] === undefined) {
|
||||
this.modelStore[collection] = {};
|
||||
}
|
||||
this.modelStore[collection][model.id] = model;
|
||||
|
||||
if (this.jsonStore[collection] === undefined) {
|
||||
this.jsonStore[collection] = {};
|
||||
}
|
||||
this.jsonStore[collection][model.id] = JSON.stringify(model);
|
||||
this.publishChangedInformation(model);
|
||||
});
|
||||
if (changeId) {
|
||||
await this.flushToStorage(changeId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* removes one or multiple models from dataStore.
|
||||
*
|
||||
* @param collectionString The desired BaseModel type to be removed from the datastore
|
||||
* @param ids A list of IDs of BaseModels to remove from the datastore
|
||||
* @param changeId The changeId of this update. If given, the storage will be flushed to the
|
||||
* cache. Else one can call {@method flushToStorage} to do this manually.
|
||||
* @example this.DS.remove('users/user', [myUser.id, 3, 4])
|
||||
*/
|
||||
public async remove(collectionString: string, ids: number[], changeId?: number): Promise<void> {
|
||||
ids.forEach(id => {
|
||||
if (this.modelStore[collectionString]) {
|
||||
delete this.modelStore[collectionString][id];
|
||||
}
|
||||
if (this.jsonStore[collectionString]) {
|
||||
delete this.jsonStore[collectionString][id];
|
||||
}
|
||||
this.publishDeletedInformation({
|
||||
collection: collectionString,
|
||||
id: id
|
||||
});
|
||||
});
|
||||
if (changeId) {
|
||||
await this.flushToStorage(changeId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the DataStore and set the given models as the new content.
|
||||
* @param models A list of models to set the DataStore to.
|
||||
* @param newMaxChangeId Optional. If given, the max change id will be updated
|
||||
* and the store flushed to the storage
|
||||
*/
|
||||
public async set(models?: BaseModel[], newMaxChangeId?: number): Promise<void> {
|
||||
const modelStoreReference = this.modelStore;
|
||||
this.modelStore = {};
|
||||
this.jsonStore = {};
|
||||
// Inform about the deletion
|
||||
Object.keys(modelStoreReference).forEach(collectionString => {
|
||||
Object.keys(modelStoreReference[collectionString]).forEach(id => {
|
||||
this.publishDeletedInformation({
|
||||
collection: collectionString,
|
||||
id: +id // needs casting, because Objects.keys gives all keys as strings...
|
||||
});
|
||||
});
|
||||
});
|
||||
if (models && models.length) {
|
||||
await this.add(models, newMaxChangeId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Informs the changed and changedOrDeleted subject about a change.
|
||||
*
|
||||
* @param model The model to publish
|
||||
*/
|
||||
private publishChangedInformation(model: BaseModel): void {
|
||||
const slot = this.DSUpdateManager.getCurrentUpdateSlot();
|
||||
if (slot) {
|
||||
slot.addChangedModel(model.collectionString, model.id);
|
||||
// triggerModifiedObservable will be called by committing the update slot.
|
||||
} else {
|
||||
this.triggerModifiedObservable();
|
||||
}
|
||||
|
||||
if (this.changedSubjects[model.collectionString]) {
|
||||
this.changedSubjects[model.collectionString].next(model);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Informs the deleted and changedOrDeleted subject about a deletion.
|
||||
*
|
||||
* @param information The information about the deleted model
|
||||
*/
|
||||
private publishDeletedInformation(information: DeletedInformation): void {
|
||||
const slot = this.DSUpdateManager.getCurrentUpdateSlot();
|
||||
if (slot) {
|
||||
slot.addDeletedModel(information.collection, information.id);
|
||||
// triggerModifiedObservable will be called by committing the update slot.
|
||||
} else {
|
||||
this.triggerModifiedObservable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the modified subject.
|
||||
*/
|
||||
public triggerModifiedObservable(): void {
|
||||
this.modifiedSubject.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the cache by inserting the serialized DataStore. Also changes the chageId, if it's larger
|
||||
* @param changeId The changeId from the update. If it's the highest change id seen, it will be set into the cache.
|
||||
*/
|
||||
public async flushToStorage(changeId: number): Promise<void> {
|
||||
this._maxChangeId = changeId;
|
||||
try {
|
||||
await this.storageService.set(DataStoreService.cachePrefix + 'DS', this.jsonStore);
|
||||
await this.storageService.set(DataStoreService.cachePrefix + 'maxChangeId', changeId);
|
||||
} catch (e) {
|
||||
if (e?.name === 'QuotaExceededError') {
|
||||
this.statusService.setTooLessLocalStorage();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public print(): void {
|
||||
console.log('Max change id', this.maxChangeId);
|
||||
console.log(JSON.stringify(this.jsonStore));
|
||||
console.log(JSON.parse(JSON.stringify(this.modelStore)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import { ErrorHandler, Injectable } from '@angular/core';
|
||||
|
||||
import { OpenSlidesStatusService } from './openslides-status.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ErrorService extends ErrorHandler {
|
||||
// TODO: This service cannot be injected into other services since it is constructed twice.
|
||||
public constructor(private statusService: OpenSlidesStatusService) {
|
||||
super();
|
||||
}
|
||||
|
||||
public handleError(error: any): void {
|
||||
const errorInformation = {
|
||||
error,
|
||||
name: this.guessName(error)
|
||||
};
|
||||
this.statusService.currentError.next(errorInformation);
|
||||
super.handleError(error);
|
||||
}
|
||||
|
||||
private guessName(error: any): string | null {
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (error.rejection?.name) {
|
||||
return error.rejection.name;
|
||||
}
|
||||
|
||||
if (error.name) {
|
||||
return error.name;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { OperatorService, Permission } from './operator.service';
|
||||
|
||||
export interface AuthGuardFallbackEntry {
|
||||
route: string;
|
||||
weight: number;
|
||||
permission: Permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Classical Auth-Guard. Checks if the user has to correct permissions to enter a page, and forwards to login if not.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FallbackRoutesService {
|
||||
private fallbackEntries: AuthGuardFallbackEntry[] = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param operator Asking for the required permission
|
||||
*/
|
||||
public constructor(private operator: OperatorService) {}
|
||||
|
||||
/**
|
||||
* Adds fallback navigation entries for the start page.
|
||||
* @param entries The entries to add
|
||||
*/
|
||||
public registerFallbackEntries(entries: AuthGuardFallbackEntry[]): void {
|
||||
this.fallbackEntries.push(...entries);
|
||||
this.fallbackEntries = this.fallbackEntries.sort((a, b) => a.weight - b.weight);
|
||||
}
|
||||
|
||||
public getFallbackRoute(): string | null {
|
||||
for (const entry of this.fallbackEntries) {
|
||||
if (this.operator.hasPerms(entry.permission)) {
|
||||
return entry.route;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
|
||||
import { StableInterceptorService } from './stable-interceptor.service';
|
||||
|
||||
export const httpInterceptorProviders = [
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: StableInterceptorService, multi: true }
|
||||
];
|
|
@ -0,0 +1,36 @@
|
|||
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { first, mergeMap } from 'rxjs/operators';
|
||||
|
||||
import { OpenSlidesStatusService } from '../openslides-status.service';
|
||||
|
||||
/**
|
||||
* An http interceptor to make sure, that every request is made
|
||||
* only when this application is stable.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StableInterceptorService implements HttpInterceptor {
|
||||
private readonly stableSubject = new BehaviorSubject<boolean>(false);
|
||||
|
||||
public constructor(private openslidesStatus: OpenSlidesStatusService) {
|
||||
this.update();
|
||||
}
|
||||
|
||||
public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
return this.stableSubject.pipe(
|
||||
first(stable => stable),
|
||||
mergeMap(() => {
|
||||
return next.handle(req);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async update(): Promise<void> {
|
||||
await this.openslidesStatus.stable;
|
||||
this.stableSubject.next(true);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HttpService } from './http.service';
|
||||
|
||||
describe('HttpService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [HttpService]
|
||||
});
|
||||
});
|
||||
// TODO: Write a working Test
|
||||
// it('should be created', () => {
|
||||
// const service: HttpService = TestBed.inject(HttpService);
|
||||
// expect(service).toBeTruthy();
|
||||
// });
|
||||
});
|
|
@ -0,0 +1,295 @@
|
|||
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { AutoupdateFormat } from '../definitions/autoupdate-format';
|
||||
import { AutoupdateThrottleService } from './autoupdate-throttle.service';
|
||||
import { HTTPMethod } from '../definitions/http-methods';
|
||||
import { OpenSlidesStatusService } from './openslides-status.service';
|
||||
import { formatQueryParams, QueryParams } from '../definitions/query-params';
|
||||
|
||||
export interface ErrorDetailResponse {
|
||||
detail: string | string[];
|
||||
args?: string[];
|
||||
}
|
||||
|
||||
function isErrorDetailResponse(obj: any): obj is ErrorDetailResponse {
|
||||
return (
|
||||
obj &&
|
||||
typeof obj === 'object' &&
|
||||
(typeof obj.detail === 'string' || obj.detail instanceof Array) &&
|
||||
(!obj.args || obj.args instanceof Array)
|
||||
);
|
||||
}
|
||||
|
||||
interface AutoupdateResponse {
|
||||
change_id: number;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
function isAutoupdateReponse(obj: any): obj is AutoupdateResponse {
|
||||
return obj && typeof obj === 'object' && typeof (obj as AutoupdateResponse).change_id === 'number';
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for managing HTTP requests. Allows to send data for every method. Also (TODO) will do generic error handling.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class HttpService {
|
||||
/**
|
||||
* http headers used by most requests
|
||||
*/
|
||||
private defaultHeaders: HttpHeaders;
|
||||
|
||||
public readonly responseChangeIds = new Subject<number>();
|
||||
|
||||
/**
|
||||
* Construct a HttpService
|
||||
*
|
||||
* Sets the default headers to application/json
|
||||
*
|
||||
* @param http The HTTP Client
|
||||
* @param translate
|
||||
* @param timeTravel requests are only allowed if history mode is disabled
|
||||
*/
|
||||
public constructor(
|
||||
private http: HttpClient,
|
||||
private translate: TranslateService,
|
||||
private OSStatus: OpenSlidesStatusService
|
||||
) {
|
||||
this.defaultHeaders = new HttpHeaders().set('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the a http request the the given path.
|
||||
* Optionally accepts a request body.
|
||||
*
|
||||
* @param path the target path, usually starting with /rest
|
||||
* @param method the required HTTP method (i.e get, post, put)
|
||||
* @param data optional, if sending a data body is required
|
||||
* @param queryParams optional queryparams to append to the path
|
||||
* @param customHeader optional custom HTTP header of required
|
||||
* @param responseType optional response type, default set to json (i.e 'arraybuffer')
|
||||
* @returns a promise containing a generic
|
||||
*/
|
||||
private async send<T>(
|
||||
path: string,
|
||||
method: HTTPMethod,
|
||||
data?: any,
|
||||
queryParams?: QueryParams,
|
||||
customHeader?: HttpHeaders,
|
||||
responseType?: string
|
||||
): Promise<T> {
|
||||
// end early, if we are in history mode
|
||||
if (this.OSStatus.isInHistoryMode && method !== HTTPMethod.GET) {
|
||||
throw this.processError('You cannot make changes while in history mode');
|
||||
}
|
||||
|
||||
// there is a current bug with the responseType.
|
||||
// https://github.com/angular/angular/issues/18586
|
||||
// castting it to 'json' allows the usage of the current array
|
||||
if (!responseType) {
|
||||
responseType = 'json';
|
||||
}
|
||||
|
||||
let url = path + formatQueryParams(queryParams);
|
||||
if (url[0] !== '/') {
|
||||
console.warn(`Please prefix the URL "${url}" with a slash.`);
|
||||
url = '/' + url;
|
||||
}
|
||||
if (this.OSStatus.isPrioritizedClient) {
|
||||
url = '/prioritize' + url;
|
||||
}
|
||||
|
||||
const options = {
|
||||
body: data,
|
||||
headers: customHeader ? customHeader : this.defaultHeaders,
|
||||
responseType: responseType as 'json'
|
||||
};
|
||||
|
||||
try {
|
||||
const responseData: T = await this.http.request<T>(method, url, options).toPromise();
|
||||
return this.processResponse(responseData);
|
||||
} catch (error) {
|
||||
throw this.processError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an error thrown by the HttpClient. Processes it to return a string that can
|
||||
* be presented to the user.
|
||||
* @param e The error thrown.
|
||||
* @returns The prepared and translated message for the user
|
||||
*/
|
||||
private processError(e: any): string {
|
||||
let error = this.translate.instant('Error') + ': ';
|
||||
// If the error is a string already, return it.
|
||||
if (typeof e === 'string') {
|
||||
return error + e;
|
||||
}
|
||||
|
||||
// If the error is no HttpErrorResponse, it's not clear what is wrong.
|
||||
if (!(e instanceof HttpErrorResponse)) {
|
||||
console.error('Unknown error thrown by the http client: ', e);
|
||||
error += this.translate.instant('An unknown error occurred.');
|
||||
return error;
|
||||
}
|
||||
|
||||
if (e.status === 405) {
|
||||
// this should only happen, if the url is wrong -> a bug.
|
||||
error += this.translate.instant(
|
||||
'The requested method is not allowed. Please contact your system administrator.'
|
||||
);
|
||||
} else if (!e.error) {
|
||||
error += this.translate.instant("The server didn't respond.");
|
||||
} else if (typeof e.error === 'object') {
|
||||
if (isErrorDetailResponse(e.error)) {
|
||||
error += this.processErrorDetailResponse(e.error);
|
||||
} else {
|
||||
const errorList = Object.keys(e.error).map(key => {
|
||||
const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
|
||||
let detail = e.error[key];
|
||||
if (detail instanceof Array) {
|
||||
detail = detail.join(' ');
|
||||
} else {
|
||||
detail = this.processErrorDetailResponse(detail);
|
||||
}
|
||||
return `${this.translate.instant(capitalizedKey)}: ${detail}`;
|
||||
});
|
||||
error = errorList.join(', ');
|
||||
}
|
||||
} else if (e.status === 500) {
|
||||
error += this.translate.instant('A server error occured. Please contact your system administrator.');
|
||||
} else if (e.status > 500) {
|
||||
error += this.translate.instant('The server could not be reached.') + ` (${e.status})`;
|
||||
} else {
|
||||
error += e.message;
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Errors from the servers may be string or array of strings. This function joins the strings together,
|
||||
* if an array is send.
|
||||
* @param str a string or a string array to join together.
|
||||
* @returns Error text(s) as single string
|
||||
*/
|
||||
private processErrorDetailResponse(response: ErrorDetailResponse): string {
|
||||
let message: string;
|
||||
if (response.detail instanceof Array) {
|
||||
message = response.detail.join(' ');
|
||||
} else {
|
||||
message = response.detail;
|
||||
}
|
||||
message = this.translate.instant(message);
|
||||
|
||||
if (response.args && response.args.length > 0) {
|
||||
for (let i = 0; i < response.args.length; i++) {
|
||||
message = message.replace(`{${i}}`, response.args[i].toString());
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
private processResponse<T>(responseData: T): T {
|
||||
if (isAutoupdateReponse(responseData)) {
|
||||
this.responseChangeIds.next(responseData.change_id);
|
||||
return responseData.data;
|
||||
}
|
||||
return responseData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a get on a path with a certain object
|
||||
* @param path The path to send the request to.
|
||||
* @param data An optional payload for the request.
|
||||
* @param queryParams Optional params appended to the path as the query part of the url.
|
||||
* @param header optional HTTP header if required
|
||||
* @param responseType option expected response type by the request (i.e 'arraybuffer')
|
||||
* @returns A promise holding a generic
|
||||
*/
|
||||
public async get<T>(
|
||||
path: string,
|
||||
data?: any,
|
||||
queryParams?: QueryParams,
|
||||
header?: HttpHeaders,
|
||||
responseType?: string
|
||||
): Promise<T> {
|
||||
return await this.send<T>(path, HTTPMethod.GET, data, queryParams, header, responseType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a post on a path with a certain object
|
||||
* @param path The path to send the request to.
|
||||
* @param data An optional payload for the request.
|
||||
* @param queryParams Optional params appended to the path as the query part of the url.
|
||||
* @param header optional HTTP header if required
|
||||
* @returns A promise holding a generic
|
||||
*/
|
||||
public async post<T>(path: string, data?: any, queryParams?: QueryParams, header?: HttpHeaders): Promise<T> {
|
||||
return await this.send<T>(path, HTTPMethod.POST, data, queryParams, header);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a put on a path with a certain object
|
||||
* @param path The path to send the request to.
|
||||
* @param data An optional payload for the request.
|
||||
* @param queryParams Optional params appended to the path as the query part of the url.
|
||||
* @param header optional HTTP header if required
|
||||
* @returns A promise holding a generic
|
||||
*/
|
||||
public async patch<T>(path: string, data?: any, queryParams?: QueryParams, header?: HttpHeaders): Promise<T> {
|
||||
return await this.send<T>(path, HTTPMethod.PATCH, data, queryParams, header);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a put on a path with a certain object
|
||||
* @param path The path to send the request to.
|
||||
* @param data An optional payload for the request.
|
||||
* @param queryParams Optional params appended to the path as the query part of the url.
|
||||
* @param header optional HTTP header if required
|
||||
* @returns A promise holding a generic
|
||||
*/
|
||||
public async put<T>(path: string, data?: any, queryParams?: QueryParams, header?: HttpHeaders): Promise<T> {
|
||||
return await this.send<T>(path, HTTPMethod.PUT, data, queryParams, header);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a delete request.
|
||||
* @param url The path to send the request to.
|
||||
* @param data An optional payload for the request.
|
||||
* @param queryParams Optional params appended to the path as the query part of the url.
|
||||
* @param header optional HTTP header if required
|
||||
* @returns A promise holding a generic
|
||||
*/
|
||||
public async delete<T>(path: string, data?: any, queryParams?: QueryParams, header?: HttpHeaders): Promise<T> {
|
||||
return await this.send<T>(path, HTTPMethod.DELETE, data, queryParams, header);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a binary file from the url and returns a base64 value
|
||||
*
|
||||
* @param url file url
|
||||
* @returns a promise with a base64 string
|
||||
*/
|
||||
public async downloadAsBase64(url: string): Promise<string> {
|
||||
return new Promise<string>(async (resolve, reject) => {
|
||||
const headers = new HttpHeaders();
|
||||
const file = await this.get<Blob>(url, {}, {}, headers, 'blob');
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => {
|
||||
const resultStr: string = reader.result as string;
|
||||
resolve(resultStr.split(',')[1]);
|
||||
};
|
||||
reader.onerror = error => {
|
||||
reject(error);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MainMenuService } from './main-menu.service';
|
||||
|
||||
describe('MainMenuService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [MainMenuService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([MainMenuService], (service: MainMenuService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,78 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { Permission } from './operator.service';
|
||||
|
||||
/**
|
||||
* This represents one entry in the main menu
|
||||
*/
|
||||
export interface MainMenuEntry {
|
||||
/**
|
||||
* The route for the router to navigate to on click.
|
||||
*/
|
||||
route: string;
|
||||
/**
|
||||
* The display string to be shown.
|
||||
*/
|
||||
displayName: string;
|
||||
|
||||
/**
|
||||
* The font awesom icon to display.
|
||||
*/
|
||||
icon: string;
|
||||
|
||||
/**
|
||||
* For sorting the entries.
|
||||
*/
|
||||
weight: number;
|
||||
|
||||
/**
|
||||
* The permission to see the entry.
|
||||
*/
|
||||
permission: Permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects main menu entries and provides them to the main menu component.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MainMenuService {
|
||||
/**
|
||||
* A list of sorted entries.
|
||||
*/
|
||||
private _entries: MainMenuEntry[] = [];
|
||||
|
||||
/**
|
||||
* Observed by the site component.
|
||||
* If a new value appears the sideNavContainer gets toggled
|
||||
*/
|
||||
public toggleMenuSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* Make the entries public.
|
||||
*/
|
||||
public get entries(): MainMenuEntry[] {
|
||||
return this._entries;
|
||||
}
|
||||
|
||||
public constructor() {}
|
||||
|
||||
/**
|
||||
* Adds entries to the mainmenu.
|
||||
* @param entries The entries to add
|
||||
*/
|
||||
public registerEntries(entries: MainMenuEntry[]): void {
|
||||
this._entries.push(...entries);
|
||||
this._entries = this._entries.sort((a, b) => a.weight - b.weight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit signal to toggle the main Menu
|
||||
*/
|
||||
public toggleMenu(): void {
|
||||
this.toggleMenuSubject.next();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from '../../../e2e-imports.module';
|
||||
import { NotifyService } from './notify.service';
|
||||
|
||||
describe('NotifyService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [NotifyService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([NotifyService], (service: NotifyService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,230 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
|
||||
import { CommunicationManagerService, OfflineError } from './communication-manager.service';
|
||||
import { HttpService } from './http.service';
|
||||
import { OperatorService } from './operator.service';
|
||||
|
||||
/**
|
||||
* Encapslates the name and content of every message regardless of being a request or response.
|
||||
*/
|
||||
interface NotifyBase<T> {
|
||||
/**
|
||||
* The name of the notify message.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The content to send.
|
||||
*/
|
||||
message: T;
|
||||
}
|
||||
|
||||
function isNotifyBase(obj: object): obj is NotifyResponse<any> {
|
||||
const base = obj as NotifyBase<any>;
|
||||
return !!obj && base.message !== undefined && base.name !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface has all fields for a notify request to the server. Next to name and content
|
||||
* one can give an array of user ids (or the value `true` for all users) and an array of
|
||||
* channel names.
|
||||
*/
|
||||
export interface NotifyRequest<T> extends NotifyBase<T> {
|
||||
channel_id: string;
|
||||
to_all?: boolean;
|
||||
|
||||
/**
|
||||
* User ids (or `true` for all users) to send this message to.
|
||||
*/
|
||||
to_users?: number[];
|
||||
|
||||
/**
|
||||
* An array of channels to send this message to.
|
||||
*/
|
||||
to_channels?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the notify-format one recieves from the server.
|
||||
*/
|
||||
export interface NotifyResponse<T> extends NotifyBase<T> {
|
||||
/**
|
||||
* This is the channel name of the one, who sends this message. Can be use to directly
|
||||
* answer this message.
|
||||
*/
|
||||
sender_channel_id: string;
|
||||
|
||||
/**
|
||||
* The user id of the user who sends this message. It is 0 for Anonymous.
|
||||
*/
|
||||
sender_user_id: number;
|
||||
|
||||
/**
|
||||
* This is validated here and is true, if the senderUserId matches the current operator's id.
|
||||
* It's also true, if one recieves a request from an anonymous and the operator itself is the anonymous.
|
||||
*/
|
||||
sendByThisUser: boolean;
|
||||
}
|
||||
|
||||
function isNotifyResponse(obj: object): obj is NotifyResponse<any> {
|
||||
const response = obj as NotifyResponse<any>;
|
||||
// Note: we do not test for sendByThisUser, since it is set later in our code.
|
||||
return isNotifyBase(obj) && response.sender_channel_id !== undefined && response.sender_user_id !== undefined;
|
||||
}
|
||||
|
||||
interface ChannelIdResponse {
|
||||
channel_id: string;
|
||||
}
|
||||
|
||||
function isChannelIdResponse(obj: object): obj is ChannelIdResponse {
|
||||
return !!obj && (obj as ChannelIdResponse).channel_id !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles all incoming and outgoing notify messages via {@link WebsocketService}.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class NotifyService {
|
||||
/**
|
||||
* A general subject for all messages.
|
||||
*/
|
||||
private notifySubject = new Subject<NotifyResponse<any>>();
|
||||
|
||||
/**
|
||||
* Subjects for specific messages.
|
||||
*/
|
||||
private messageSubjects: {
|
||||
[name: string]: Subject<NotifyResponse<any>>;
|
||||
} = {};
|
||||
|
||||
private channelId: string;
|
||||
|
||||
public constructor(
|
||||
private communicationManager: CommunicationManagerService,
|
||||
private http: HttpService,
|
||||
private operator: OperatorService
|
||||
) {
|
||||
this.communicationManager.startCommunicationEvent.subscribe(() => this.startListening());
|
||||
this.communicationManager.stopCommunicationEvent.subscribe(() => (this.channelId = null));
|
||||
}
|
||||
|
||||
private async startListening(): Promise<void> {
|
||||
try {
|
||||
await this.communicationManager.subscribe<NotifyResponse<any> | ChannelIdResponse>(
|
||||
'/system/notify',
|
||||
notify => {
|
||||
if (isChannelIdResponse(notify)) {
|
||||
this.channelId = notify.channel_id;
|
||||
} else if (isNotifyResponse(notify)) {
|
||||
notify.sendByThisUser =
|
||||
notify.sender_user_id === (this.operator.user ? this.operator.user.id : 0);
|
||||
this.notifySubject.next(notify);
|
||||
if (this.messageSubjects[notify.name]) {
|
||||
this.messageSubjects[notify.name].next(notify);
|
||||
}
|
||||
} else {
|
||||
console.error('Unknwon notify message', notify);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
if (!(e instanceof OfflineError)) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sents a notify message to all users (so all clients that are online).
|
||||
* @param name The name of the notify message
|
||||
* @param content The payload to send
|
||||
*/
|
||||
public async sendToAllUsers<T>(name: string, content: T): Promise<void> {
|
||||
await this.send(name, content, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notify message to all open clients with the given users logged in.
|
||||
* @param name The name of th enotify message
|
||||
* @param content The payload to send.
|
||||
* @param users Multiple user ids.
|
||||
*/
|
||||
public async sendToUsers<T>(name: string, content: T, ...users: number[]): Promise<void> {
|
||||
if (users.length < 1) {
|
||||
throw new Error('You have to provide at least one user');
|
||||
}
|
||||
await this.send(name, content, false, users);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notify message to all given channels.
|
||||
* @param name The name of th enotify message
|
||||
* @param content The payload to send.
|
||||
* @param channels Multiple channels to send this message to.
|
||||
*/
|
||||
public async sendToChannels<T>(name: string, content: T, ...channels: string[]): Promise<void> {
|
||||
if (channels.length < 1) {
|
||||
throw new Error('You have to provide at least one channel');
|
||||
}
|
||||
await this.send(name, content, false, null, channels);
|
||||
}
|
||||
|
||||
/**
|
||||
* General send function for notify messages.
|
||||
* @param name The name of the notify message
|
||||
* @param message The payload to send.
|
||||
* @param users Either an array of IDs or `true` meaning of sending this message to all online users clients.
|
||||
* @param channels An array of channels to send this message to.
|
||||
*/
|
||||
private async send<T>(
|
||||
name: string,
|
||||
message: T,
|
||||
toAll?: boolean,
|
||||
users?: number[],
|
||||
channels?: string[]
|
||||
): Promise<void> {
|
||||
if (!this.channelId) {
|
||||
throw new Error('No channel id!');
|
||||
}
|
||||
|
||||
const notify: NotifyRequest<T> = {
|
||||
name: name,
|
||||
message: message,
|
||||
channel_id: this.channelId
|
||||
};
|
||||
if (toAll === true) {
|
||||
notify.to_all = true;
|
||||
}
|
||||
if (users) {
|
||||
notify.to_users = users;
|
||||
}
|
||||
if (channels) {
|
||||
notify.to_channels = channels;
|
||||
}
|
||||
|
||||
console.debug('send notify', notify);
|
||||
await this.http.post<unknown>('/system/notify/send', notify);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a general observalbe of all notify messages.
|
||||
*/
|
||||
public getObservable(): Observable<NotifyResponse<any>> {
|
||||
return this.notifySubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable for a specific type of messages.
|
||||
* @param name The name of all messages to observe.
|
||||
*/
|
||||
public getMessageObservable<T>(name: string): Observable<NotifyResponse<T>> {
|
||||
if (!this.messageSubjects[name]) {
|
||||
this.messageSubjects[name] = new Subject<NotifyResponse<any>>();
|
||||
}
|
||||
return this.messageSubjects[name].asObservable() as Observable<NotifyResponse<T>>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { EventEmitter, Injectable } from '@angular/core';
|
||||
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
export enum OfflineReason {
|
||||
WhoAmIFailed,
|
||||
ConnectionLost
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class OfflineBroadcastService {
|
||||
public readonly isOfflineSubject = new BehaviorSubject<boolean>(false);
|
||||
public get isOfflineObservable(): Observable<boolean> {
|
||||
return this.isOfflineSubject.asObservable();
|
||||
}
|
||||
|
||||
private readonly _goOffline = new EventEmitter<OfflineReason>();
|
||||
public get goOfflineObservable(): Observable<OfflineReason> {
|
||||
return this._goOffline.asObservable();
|
||||
}
|
||||
|
||||
public constructor() {}
|
||||
|
||||
public goOffline(reason: OfflineReason): void {
|
||||
this._goOffline.emit(reason);
|
||||
}
|
||||
|
||||
public isOffline(): boolean {
|
||||
return this.isOfflineSubject.getValue();
|
||||
}
|
||||
|
||||
public isOnline(): boolean {
|
||||
return !this.isOffline();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { OfflineService } from './offline.service';
|
||||
|
||||
describe('OfflineService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [OfflineService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([OfflineService], (service: OfflineService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,125 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { CommunicationManagerService } from './communication-manager.service';
|
||||
import { OfflineBroadcastService, OfflineReason } from './offline-broadcast.service';
|
||||
import { OpenSlidesService } from './openslides.service';
|
||||
import { OperatorService, WhoAmI } from './operator.service';
|
||||
|
||||
/**
|
||||
* This service handles everything connected with being offline.
|
||||
*
|
||||
* TODO: This is just a stub. Needs to be done in the future; Maybe we cancel this whole concept
|
||||
* of this service. We'll see what happens here..
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class OfflineService {
|
||||
private reason: OfflineReason | null;
|
||||
|
||||
public constructor(
|
||||
private OpenSlides: OpenSlidesService,
|
||||
private offlineBroadcastService: OfflineBroadcastService,
|
||||
private operatorService: OperatorService,
|
||||
private communicationManager: CommunicationManagerService
|
||||
) {
|
||||
this.offlineBroadcastService.goOfflineObservable.subscribe((reason: OfflineReason) => this.goOffline(reason));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to set offline status
|
||||
*/
|
||||
public goOffline(reason: OfflineReason): void {
|
||||
if (this.offlineBroadcastService.isOffline()) {
|
||||
return;
|
||||
}
|
||||
this.reason = reason;
|
||||
|
||||
if (reason === OfflineReason.ConnectionLost) {
|
||||
console.log('offline because connection lost.');
|
||||
} else if (reason === OfflineReason.WhoAmIFailed) {
|
||||
console.log('offline because whoami failed.');
|
||||
} else {
|
||||
console.error('No such offline reason', reason);
|
||||
}
|
||||
|
||||
this.offlineBroadcastService.isOfflineSubject.next(true);
|
||||
this.checkStillOffline();
|
||||
}
|
||||
|
||||
private checkStillOffline(): void {
|
||||
const timeout = Math.floor(Math.random() * 3000 + 2000);
|
||||
console.log(`Try to go online in ${timeout} ms`);
|
||||
|
||||
setTimeout(async () => {
|
||||
let online: boolean;
|
||||
let whoami: WhoAmI | null = null;
|
||||
|
||||
if (this.reason === OfflineReason.ConnectionLost) {
|
||||
online = await this.communicationManager.isCommunicationServiceOnline();
|
||||
console.log('is communication online? ', online);
|
||||
} else if (this.reason === OfflineReason.WhoAmIFailed) {
|
||||
const result = await this.operatorService.whoAmI();
|
||||
online = result.online;
|
||||
whoami = result.whoami;
|
||||
console.log('is whoami reachable?', online);
|
||||
}
|
||||
|
||||
if (online) {
|
||||
await this.goOnline(whoami);
|
||||
// TODO: check all other reasons -> e.g. if the
|
||||
// connection was lost, the operator must be checked and the other way
|
||||
// around the comminucation must be started!!
|
||||
|
||||
// stop trying.
|
||||
} else {
|
||||
// continue trying.
|
||||
this.checkStillOffline();
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to return to online-status.
|
||||
*
|
||||
* First, we have to check, if all other sources (except this.reason) are online, too.
|
||||
* This results in definetly having a whoami response at this point.
|
||||
* If this is the case, we need to setup everything again:
|
||||
* 1) check the operator. If this allowes for an logged in state (or anonymous is OK), do
|
||||
* step 2, otherwise done.
|
||||
* 2) enable communications.
|
||||
*/
|
||||
private async goOnline(whoami?: WhoAmI): Promise<void> {
|
||||
console.log('go online!', this.reason, whoami);
|
||||
if (this.reason === OfflineReason.ConnectionLost) {
|
||||
// now we have to check whoami
|
||||
const result = await this.operatorService.whoAmI();
|
||||
if (!result.online) {
|
||||
console.log('whoami down.');
|
||||
this.reason = OfflineReason.WhoAmIFailed;
|
||||
this.checkStillOffline();
|
||||
return;
|
||||
}
|
||||
whoami = result.whoami;
|
||||
} else if (this.reason === OfflineReason.WhoAmIFailed) {
|
||||
const online = await this.communicationManager.isCommunicationServiceOnline();
|
||||
if (!online) {
|
||||
console.log('communication down.');
|
||||
this.reason = OfflineReason.ConnectionLost;
|
||||
this.checkStillOffline();
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.log('we are online!');
|
||||
|
||||
// Ok, we are online now!
|
||||
const isLoggedIn = await this.OpenSlides.checkWhoAmI(whoami);
|
||||
console.log('logged in:', isLoggedIn);
|
||||
if (isLoggedIn) {
|
||||
this.communicationManager.startCommunication();
|
||||
}
|
||||
console.log('done');
|
||||
|
||||
this.offlineBroadcastService.isOfflineSubject.next(false);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { OpenSlidesStatusService } from './openslides-status.service';
|
||||
|
||||
describe('OpenSlidesStatusService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [OpenSlidesStatusService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([OpenSlidesStatusService], (service: OpenSlidesStatusService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { History } from 'app/shared/models/core/history';
|
||||
import { BannerDefinition, BannerService } from '../ui-services/banner.service';
|
||||
import { Deferred } from '../promises/deferred';
|
||||
|
||||
export interface ErrorInformation {
|
||||
error: any;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds information about OpenSlides. This is not included into other services to
|
||||
* avoid circular dependencies.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class OpenSlidesStatusService {
|
||||
/**
|
||||
* in History mode, saves the history point.
|
||||
*/
|
||||
private history: History = null;
|
||||
private historyBanner: BannerDefinition = {
|
||||
type: 'history'
|
||||
};
|
||||
|
||||
private tooLessLocalStorage = false;
|
||||
|
||||
/**
|
||||
* Returns, if OpenSlides is in the history mode.
|
||||
*/
|
||||
public get isInHistoryMode(): boolean {
|
||||
return !!this.history;
|
||||
}
|
||||
|
||||
public get stable(): Promise<void> {
|
||||
return this._stable;
|
||||
}
|
||||
|
||||
public isPrioritizedClient = false;
|
||||
|
||||
public readonly currentError = new BehaviorSubject<ErrorInformation | null>(null);
|
||||
|
||||
private _stable = new Deferred();
|
||||
private _bootedSubject = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/**
|
||||
* Ctor, does nothing.
|
||||
*/
|
||||
public constructor(private banner: BannerService) {}
|
||||
|
||||
public setStable(): void {
|
||||
this._stable.resolve();
|
||||
this._bootedSubject.next(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the getLocaleString function of the history object, if present.
|
||||
*
|
||||
* @param format the required date representation format
|
||||
* @returns the timestamp as string
|
||||
*/
|
||||
public getHistoryTimeStamp(format: string): string {
|
||||
return this.history ? this.history.getLocaleString(format) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enters the history mode
|
||||
*/
|
||||
public enterHistoryMode(history: History): void {
|
||||
this.history = history;
|
||||
this.banner.addBanner(this.historyBanner);
|
||||
}
|
||||
|
||||
/**
|
||||
* Leaves the history mode
|
||||
*/
|
||||
public leaveHistoryMode(): void {
|
||||
this.history = null;
|
||||
this.banner.removeBanner(this.historyBanner);
|
||||
}
|
||||
|
||||
public setTooLessLocalStorage(): void {
|
||||
if (!this.tooLessLocalStorage) {
|
||||
this.tooLessLocalStorage = true;
|
||||
this.banner.addBanner({ type: 'tooLessLocalStorage' });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from '../../../e2e-imports.module';
|
||||
import { OpenSlidesService } from './openslides.service';
|
||||
|
||||
describe('OpenSlidesService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [OpenSlidesService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([OpenSlidesService], (service: OpenSlidesService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,186 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { CommunicationManagerService } from './communication-manager.service';
|
||||
import { DataStoreService } from './data-store.service';
|
||||
import { OfflineBroadcastService, OfflineReason } from './offline-broadcast.service';
|
||||
import { OpenSlidesStatusService } from './openslides-status.service';
|
||||
import { OperatorService, WhoAmI } from './operator.service';
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
/**
|
||||
* Handles the bootup/showdown of this application.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class OpenSlidesService {
|
||||
/**
|
||||
* If the user tries to access a certain URL without being authenticated, the URL will be stored here
|
||||
*/
|
||||
public redirectUrl: string;
|
||||
|
||||
/**
|
||||
* Subject to hold the flag `booted`.
|
||||
*/
|
||||
public readonly booted = new BehaviorSubject(false);
|
||||
|
||||
/**
|
||||
* Saves, if OpenSlides is fully booted. This means, that a user must be logged in
|
||||
* (Anonymous is also a user in this case). This is the case after `afterLoginBootup`.
|
||||
*/
|
||||
public get isBooted(): boolean {
|
||||
return this.booted.value;
|
||||
}
|
||||
|
||||
public constructor(
|
||||
private storageService: StorageService,
|
||||
private operator: OperatorService,
|
||||
private openslidesStatus: OpenSlidesStatusService,
|
||||
private router: Router,
|
||||
private DS: DataStoreService,
|
||||
private communicationManager: CommunicationManagerService,
|
||||
private offlineBroadcastService: OfflineBroadcastService
|
||||
) {
|
||||
this.bootup();
|
||||
}
|
||||
|
||||
/**
|
||||
* the bootup-sequence: Do a whoami request and if it was successful, do
|
||||
* {@method afterLoginBootup}. If not, redirect the user to the login page.
|
||||
*/
|
||||
public async bootup(): Promise<void> {
|
||||
// start autoupdate if the user is logged in:
|
||||
let whoami = await this.operator.whoAmIFromStorage();
|
||||
const needToCheckOperator = !!whoami;
|
||||
|
||||
if (!whoami) {
|
||||
const response = await this.operator.whoAmI();
|
||||
if (!response.online) {
|
||||
this.offlineBroadcastService.goOffline(OfflineReason.WhoAmIFailed);
|
||||
}
|
||||
whoami = response.whoami;
|
||||
}
|
||||
|
||||
if (!whoami.user && !whoami.guest_enabled) {
|
||||
if (!location.pathname.includes('error')) {
|
||||
this.redirectUrl = location.pathname;
|
||||
}
|
||||
this.redirectToLoginIfNotSubpage();
|
||||
} else {
|
||||
await this.afterLoginBootup(whoami.user_id);
|
||||
}
|
||||
|
||||
if (needToCheckOperator) {
|
||||
// Check for the operator via a async whoami (so no await here)
|
||||
// to validate, that the cache was correct.
|
||||
this.checkOperator(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects the user to /login, if he isn't on a subpage.
|
||||
*/
|
||||
private redirectToLoginIfNotSubpage(): void {
|
||||
if (!this.redirectUrl || !this.redirectUrl.includes('/login/')) {
|
||||
// Goto login, if the user isn't on a subpage like
|
||||
// legal notice or reset passwort view.
|
||||
// If other routing requests are active (e.g. to `/` or `/error`)
|
||||
// wait for the authguard to finish to navigate to /login. This
|
||||
// redirect is more important than the other ones.
|
||||
setTimeout(() => {
|
||||
this.router.navigate(['/login']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* the login bootup-sequence: Check (and maybe clear) the cache und setup the DataStore
|
||||
* and websocket. This "login" also may be the "login" of an anonymous when he is using
|
||||
* OpenSlides as a guest.
|
||||
* @param userId the id or null for guest
|
||||
*/
|
||||
public async afterLoginBootup(userId: number | null): Promise<void> {
|
||||
// Check, which user was logged in last time
|
||||
const lastUserId = await this.storageService.get<number>('lastUserLoggedIn');
|
||||
// if the user changed, reset the cache and save the new user.
|
||||
if (userId !== lastUserId) {
|
||||
await this.DS.clear();
|
||||
await this.storageService.set('lastUserLoggedIn', userId);
|
||||
}
|
||||
await this.setupDataStoreAndStartCommunication();
|
||||
// Now finally booted.
|
||||
this.booted.next(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Init DS from cache and after this start the websocket service.
|
||||
*/
|
||||
private async setupDataStoreAndStartCommunication(): Promise<void> {
|
||||
await this.DS.initFromStorage();
|
||||
await this.openslidesStatus.stable;
|
||||
this.communicationManager.startCommunication();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down OpenSlides.
|
||||
*/
|
||||
public async shutdown(): Promise<void> {
|
||||
this.communicationManager.closeConnections();
|
||||
this.booted.next(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown and bootup.
|
||||
*/
|
||||
public async reboot(): Promise<void> {
|
||||
await this.shutdown();
|
||||
await this.bootup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the client cache and restarts OpenSlides. Results in "flickering" of the
|
||||
* login mask, because the cached operator is also cleared.
|
||||
*/
|
||||
public async reset(): Promise<void> {
|
||||
await this.shutdown();
|
||||
await this.storageService.clear();
|
||||
await this.bootup();
|
||||
}
|
||||
|
||||
public async checkOperator(requestChanges: boolean = true): Promise<void> {
|
||||
const response = await this.operator.whoAmI();
|
||||
if (!response.online) {
|
||||
this.offlineBroadcastService.goOffline(OfflineReason.WhoAmIFailed);
|
||||
}
|
||||
await this.checkWhoAmI(response.whoami, requestChanges);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the operator is the same as it was before. Should be alled on a reconnect.
|
||||
*
|
||||
* @returns true, if the user is still logged in
|
||||
*/
|
||||
public async checkWhoAmI(whoami: WhoAmI, requestChanges: boolean = true): Promise<boolean> {
|
||||
let isLoggedIn = false;
|
||||
// User logged off.
|
||||
if (!whoami.user && !whoami.guest_enabled) {
|
||||
await this.shutdown();
|
||||
this.redirectToLoginIfNotSubpage();
|
||||
} else {
|
||||
isLoggedIn = true;
|
||||
if (
|
||||
(this.operator.user && this.operator.user.id !== whoami.user_id) ||
|
||||
(!this.operator.user && whoami.user_id)
|
||||
) {
|
||||
// user changed
|
||||
await this.DS.clear();
|
||||
await this.reboot();
|
||||
}
|
||||
}
|
||||
|
||||
return isLoggedIn;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from '../../../e2e-imports.module';
|
||||
import { OperatorService } from './operator.service';
|
||||
|
||||
describe('OperatorService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [OperatorService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([OperatorService], (service: OperatorService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,483 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { environment } from 'environments/environment';
|
||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||
import { auditTime, filter } from 'rxjs/operators';
|
||||
|
||||
import { Group } from 'app/shared/models/users/group';
|
||||
import { ViewUser } from 'app/site/users/models/view-user';
|
||||
import { CollectionStringMapperService } from './collection-string-mapper.service';
|
||||
import { DataStoreService } from './data-store.service';
|
||||
import { Deferred } from '../promises/deferred';
|
||||
import { HttpService } from './http.service';
|
||||
import { OnAfterAppsLoaded } from '../definitions/on-after-apps-loaded';
|
||||
import { OpenSlidesStatusService } from './openslides-status.service';
|
||||
import { StorageService } from './storage.service';
|
||||
import { DEFAULT_AUTH_TYPE, User, UserAuthType } from '../../shared/models/users/user';
|
||||
import { UserRepositoryService } from '../repositories/users/user-repository.service';
|
||||
|
||||
/**
|
||||
* Permissions on the client are just strings. This makes clear, that
|
||||
* permissions instead of arbitrary strings should be given.
|
||||
*/
|
||||
export enum Permission {
|
||||
agendaCanManage = 'agenda.can_manage',
|
||||
agendaCanSee = 'agenda.can_see',
|
||||
agendaCanSeeInternalItems = 'agenda.can_see_internal_items',
|
||||
agendaCanManageListOfSpeakers = 'agenda.can_manage_list_of_speakers',
|
||||
agendaCanSeeListOfSpeakers = 'agenda.can_see_list_of_speakers',
|
||||
agendaCanBeSpeaker = 'agenda.can_be_speaker',
|
||||
assignmentsCanManage = 'assignments.can_manage',
|
||||
assignmentsCanNominateOther = 'assignments.can_nominate_other',
|
||||
assignmentsCanNominateSelf = 'assignments.can_nominate_self',
|
||||
assignmentsCanSee = 'assignments.can_see',
|
||||
coreCanManageConfig = 'core.can_manage_config',
|
||||
coreCanManageLogosAndFonts = 'core.can_manage_logos_and_fonts',
|
||||
coreCanSeeHistory = 'core.can_see_history',
|
||||
coreCanManageProjector = 'core.can_manage_projector',
|
||||
coreCanSeeFrontpage = 'core.can_see_frontpage',
|
||||
coreCanSeeProjector = 'core.can_see_projector',
|
||||
coreCanManageTags = 'core.can_manage_tags',
|
||||
coreCanSeeLiveStream = 'core.can_see_livestream',
|
||||
coreCanSeeAutopilot = 'core.can_see_autopilot',
|
||||
mediafilesCanManage = 'mediafiles.can_manage',
|
||||
mediafilesCanSee = 'mediafiles.can_see',
|
||||
motionsCanCreate = 'motions.can_create',
|
||||
motionsCanCreateAmendments = 'motions.can_create_amendments',
|
||||
motionsCanManage = 'motions.can_manage',
|
||||
motionsCanManageMetadata = 'motions.can_manage_metadata',
|
||||
motionsCanManagePolls = 'motions.can_manage_polls',
|
||||
motionsCanSee = 'motions.can_see',
|
||||
motionsCanSeeInternal = 'motions.can_see_internal',
|
||||
motionsCanSupport = 'motions.can_support',
|
||||
usersCanChangePassword = 'users.can_change_password',
|
||||
usersCanManage = 'users.can_manage',
|
||||
usersCanSeeExtraData = 'users.can_see_extra_data',
|
||||
usersCanSeeName = 'users.can_see_name',
|
||||
chatCanManage = 'chat.can_manage'
|
||||
}
|
||||
|
||||
/**
|
||||
* Response format of the WhoAmI request.
|
||||
*/
|
||||
export interface WhoAmI {
|
||||
user_id: number;
|
||||
guest_enabled: boolean;
|
||||
user: User;
|
||||
auth_type: UserAuthType;
|
||||
permissions: Permission[];
|
||||
}
|
||||
|
||||
function isWhoAmI(obj: any): obj is WhoAmI {
|
||||
if (!obj) {
|
||||
return false;
|
||||
}
|
||||
const whoAmI = obj as WhoAmI;
|
||||
return (
|
||||
whoAmI.guest_enabled !== undefined &&
|
||||
whoAmI.user !== undefined &&
|
||||
whoAmI.user_id !== undefined &&
|
||||
whoAmI.permissions !== undefined &&
|
||||
whoAmI.auth_type !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
const WHOAMI_STORAGE_KEY = 'whoami';
|
||||
|
||||
/**
|
||||
* The operator represents the user who is using OpenSlides.
|
||||
*
|
||||
* Changes in operator can be observed, directives do so on order to show
|
||||
* or hide certain information.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class OperatorService implements OnAfterAppsLoaded {
|
||||
/**
|
||||
* The operator.
|
||||
*/
|
||||
private _user: User;
|
||||
|
||||
public get user(): User {
|
||||
return this._user;
|
||||
}
|
||||
|
||||
/**
|
||||
* The operator as a view user. We need a separation here, because
|
||||
* we need to acces the operators permissions, before we get data
|
||||
* from the server to build the view user.
|
||||
*/
|
||||
private _viewUser: ViewUser;
|
||||
|
||||
/**
|
||||
* Get the user that corresponds to operator.
|
||||
*/
|
||||
public get viewUser(): ViewUser {
|
||||
return this._viewUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 0 for the anonymous
|
||||
*/
|
||||
public get userId(): number {
|
||||
return this.user?.id || 0;
|
||||
}
|
||||
|
||||
public get isAnonymous(): boolean {
|
||||
return !this.user || this.user.id === 0;
|
||||
}
|
||||
|
||||
public get isSuperAdmin(): boolean {
|
||||
return this.isInGroupIdsNonAdminCheck(2);
|
||||
}
|
||||
|
||||
public readonly authType: BehaviorSubject<UserAuthType> = new BehaviorSubject(DEFAULT_AUTH_TYPE);
|
||||
|
||||
/**
|
||||
* Save, if guests are enabled.
|
||||
*/
|
||||
public get guestsEnabled(): boolean {
|
||||
return this.currentWhoAmI ? this.currentWhoAmI.guest_enabled : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The permissions of the operator. Updated via {@method updatePermissions}.
|
||||
*/
|
||||
private permissions: Permission[] = [];
|
||||
|
||||
/**
|
||||
* The subject that can be observed by other instances using observing functions.
|
||||
*/
|
||||
private operatorSubject: BehaviorSubject<User> = new BehaviorSubject<User>(null);
|
||||
|
||||
/**
|
||||
* Subject for the operator as a view user.
|
||||
*/
|
||||
private viewOperatorSubject: BehaviorSubject<ViewUser> = new BehaviorSubject<ViewUser>(null);
|
||||
|
||||
/**
|
||||
* Do not access the repo before it wasn't loaded. Will be true after `onAfterAppsLoaded`.
|
||||
*/
|
||||
private userRepository: UserRepositoryService | null;
|
||||
|
||||
private _currentWhoAmI: WhoAmI | null = null;
|
||||
private _defaultWhoAmI: WhoAmI = {
|
||||
user_id: null,
|
||||
guest_enabled: false,
|
||||
user: null,
|
||||
auth_type: DEFAULT_AUTH_TYPE,
|
||||
permissions: []
|
||||
};
|
||||
|
||||
/**
|
||||
* The current WhoAmI response to extract the user (the operator) from.
|
||||
*/
|
||||
private get currentWhoAmI(): WhoAmI {
|
||||
return this._currentWhoAmI || this._defaultWhoAmI;
|
||||
}
|
||||
|
||||
private set currentWhoAmI(value: WhoAmI | null) {
|
||||
this._currentWhoAmI = value;
|
||||
|
||||
// Resetting the default whoami, when the current whoami isn't there. This
|
||||
// is for a fresh restart and do not have (old) changed values in this.defaultWhoAmI
|
||||
if (!value) {
|
||||
this._defaultWhoAmI = this.getDefaultWhoAmIResponse();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly _loaded: Deferred<void> = new Deferred();
|
||||
|
||||
public get loaded(): Promise<void> {
|
||||
return this._loaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is for the viewUser check, if the user id has changed, on a user update.
|
||||
*/
|
||||
private lastUserId: number | null = null;
|
||||
|
||||
/**
|
||||
* The subscription to the viewuser from the user repository.
|
||||
*/
|
||||
private viewOperatorSubscription: Subscription;
|
||||
|
||||
/**
|
||||
* Sets up an observer for watching changes in the DS. If the operator user or groups are changed,
|
||||
* the operator's permissions are updated.
|
||||
*
|
||||
* @param http
|
||||
* @param DS
|
||||
* @param offlineService
|
||||
*/
|
||||
public constructor(
|
||||
private http: HttpService,
|
||||
private DS: DataStoreService,
|
||||
private collectionStringMapper: CollectionStringMapperService,
|
||||
private storageService: StorageService,
|
||||
private OSStatus: OpenSlidesStatusService
|
||||
) {
|
||||
this.DS.getChangeObservable(User).subscribe(newModel => {
|
||||
if (this._user && this._user.id === newModel.id) {
|
||||
this._user = newModel;
|
||||
this.updateUserInCurrentWhoAmI();
|
||||
}
|
||||
});
|
||||
this.DS.getChangeObservable(Group)
|
||||
.pipe(
|
||||
filter(
|
||||
model =>
|
||||
// Any group has changed if we have an operator or
|
||||
// group 1 (default) for anonymous changed
|
||||
!!this._user || model.id === 1
|
||||
),
|
||||
auditTime(10)
|
||||
)
|
||||
.subscribe(() => this.updatePermissions());
|
||||
|
||||
// Watches the user observable to update the viewUser for the operator.
|
||||
this.getUserObservable().subscribe(user => {
|
||||
const userId = user ? user.id : null;
|
||||
if ((!user && this.lastUserId === null) || userId === this.lastUserId) {
|
||||
return; // The user didn't changed.
|
||||
}
|
||||
this.lastUserId = userId;
|
||||
|
||||
// User changed: clear subscription and subscribe to the new user (if there is one)
|
||||
if (this.viewOperatorSubscription) {
|
||||
this.viewOperatorSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
if (user && this.userRepository) {
|
||||
this.viewOperatorSubscription = this.userRepository
|
||||
.getViewModelObservable(user.id)
|
||||
.subscribe(viewUser => {
|
||||
this._viewUser = viewUser;
|
||||
this.viewOperatorSubject.next(viewUser);
|
||||
});
|
||||
} else {
|
||||
// The operator is anonymous.
|
||||
this.viewOperatorSubject.next(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the repo to get a view user.
|
||||
*/
|
||||
public onAfterAppsLoaded(): void {
|
||||
this.userRepository = this.collectionStringMapper.getRepository(ViewUser) as UserRepositoryService;
|
||||
if (this.user) {
|
||||
this._viewUser = this.userRepository.getViewModel(this.user.id);
|
||||
}
|
||||
this.viewOperatorSubject.next(this._viewUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current WhoAmI response from the storage.
|
||||
*/
|
||||
public async whoAmIFromStorage(): Promise<WhoAmI | null> {
|
||||
let response: WhoAmI | null = null;
|
||||
try {
|
||||
response = await this.storageService.get<WhoAmI>(WHOAMI_STORAGE_KEY);
|
||||
if (!isWhoAmI(response)) {
|
||||
response = null;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
if (response) {
|
||||
await this.updateCurrentWhoAmI(response);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public async clearWhoAmIFromStorage(): Promise<void> {
|
||||
await this.storageService.remove(WHOAMI_STORAGE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the operator user. Will be saved to storage
|
||||
* @param user The new operator.
|
||||
*/
|
||||
public async setWhoAmI(whoami: WhoAmI | null): Promise<void> {
|
||||
if (whoami === null) {
|
||||
whoami = this.getDefaultWhoAmIResponse();
|
||||
}
|
||||
await this.updateCurrentWhoAmI(whoami);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls `/apps/users/whoami` to find out the real operator.
|
||||
*
|
||||
* @returns The response of the WhoAmI request.
|
||||
*/
|
||||
public async whoAmI(): Promise<{ whoami: WhoAmI; online: boolean }> {
|
||||
let online = true;
|
||||
try {
|
||||
const response = await this.http.get(environment.urlPrefix + '/users/whoami/');
|
||||
if (isWhoAmI(response)) {
|
||||
await this.updateCurrentWhoAmI(response);
|
||||
} else {
|
||||
online = false;
|
||||
}
|
||||
} catch (e) {
|
||||
online = false;
|
||||
}
|
||||
return { whoami: this.currentWhoAmI, online };
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the user to storage by wrapping it into a (maybe existing)
|
||||
* WhoAMI response.
|
||||
*/
|
||||
private async updateUserInCurrentWhoAmI(): Promise<void> {
|
||||
if (this.isAnonymous) {
|
||||
this.currentWhoAmI.user_id = null;
|
||||
this.currentWhoAmI.user = null;
|
||||
} else {
|
||||
this.currentWhoAmI.user_id = this.user.id;
|
||||
this.currentWhoAmI.user = this.user;
|
||||
}
|
||||
this.currentWhoAmI.permissions = this.permissions;
|
||||
await this.updateCurrentWhoAmI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the user and update the permissions.
|
||||
*/
|
||||
private async updateCurrentWhoAmI(whoami?: WhoAmI): Promise<void> {
|
||||
if (whoami) {
|
||||
this.currentWhoAmI = whoami;
|
||||
} else {
|
||||
whoami = this.currentWhoAmI;
|
||||
}
|
||||
|
||||
this._user = whoami ? whoami.user : null;
|
||||
this.authType.next(whoami ? whoami.auth_type : DEFAULT_AUTH_TYPE);
|
||||
await this.updatePermissions();
|
||||
this._loaded.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns an observable for the operator as a user.
|
||||
*/
|
||||
public getUserObservable(): Observable<User> {
|
||||
return this.operatorSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns an observable for the operator as a viewUser. Note, that
|
||||
* the viewUser might not be there, so for reliable (and not display) information,
|
||||
* use the `getUserObservable`.
|
||||
*/
|
||||
public getViewUserObservable(): Observable<ViewUser> {
|
||||
return this.viewOperatorSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, if the operator has at least one of the given permissions.
|
||||
* @param checkPerms The permissions to check, if at least one matches.
|
||||
*/
|
||||
public hasPerms(...checkPerms: Permission[]): boolean {
|
||||
if (this._user && this._user.groups_id.includes(2)) {
|
||||
return true;
|
||||
}
|
||||
return checkPerms.some(permission => {
|
||||
return this.permissions.includes(permission);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true, if the operator is in at least one group or he is in the admin group.
|
||||
* @param groups The groups to check
|
||||
*/
|
||||
public isInGroup(...groups: Group[]): boolean {
|
||||
return this.isInGroupIds(...groups.map(group => group.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true, if the operator is in at least one group or he is in the admin group.
|
||||
* @param groups The group ids to check
|
||||
*/
|
||||
public isInGroupIds(...groupIds: number[]): boolean {
|
||||
if (!this.isInGroupIdsNonAdminCheck(...groupIds)) {
|
||||
// An admin has all perms and is technically in every group.
|
||||
return this.user && this.user.groups_id.includes(2);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true, if the operator is in at least one group.
|
||||
* @param groups The group ids to check
|
||||
*/
|
||||
public isInGroupIdsNonAdminCheck(...groupIds: number[]): boolean {
|
||||
if (!this.user) {
|
||||
return groupIds.includes(1); // any anonymous is in the default group.
|
||||
}
|
||||
return groupIds.some(id => this.user.groups_id.includes(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the operators permissions and publish the operator afterwards.
|
||||
* Saves the current WhoAmI to storage with the updated permissions
|
||||
*/
|
||||
private async updatePermissions(): Promise<void> {
|
||||
this.permissions = [];
|
||||
|
||||
// If we do not have any groups, take the permissions from the
|
||||
// latest WhoAmI response.
|
||||
if (this.DS.getAll(Group).length === 0) {
|
||||
if (this.currentWhoAmI) {
|
||||
this.permissions = this.currentWhoAmI.permissions;
|
||||
}
|
||||
} else {
|
||||
// Anonymous or users in the default group.
|
||||
if (!this.user || this.user.groups_id.length === 0) {
|
||||
const defaultGroup: Group = this.DS.get<Group>('users/group', 1);
|
||||
if (defaultGroup && defaultGroup.permissions instanceof Array) {
|
||||
this.permissions = defaultGroup.permissions;
|
||||
}
|
||||
} else {
|
||||
const permissionSet = new Set<Permission>();
|
||||
this.DS.getMany(Group, this.user.groups_id).forEach(group => {
|
||||
group.permissions.forEach(permission => {
|
||||
permissionSet.add(permission);
|
||||
});
|
||||
});
|
||||
this.permissions = Array.from(permissionSet.values());
|
||||
}
|
||||
}
|
||||
|
||||
// Save perms to current WhoAmI
|
||||
this.currentWhoAmI.permissions = this.permissions;
|
||||
|
||||
if (!this.OSStatus.isInHistoryMode) {
|
||||
await this.storageService.set(WHOAMI_STORAGE_KEY, this.currentWhoAmI);
|
||||
}
|
||||
|
||||
// publish changes in the operator.
|
||||
this.operatorSubject.next(this.user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the operators presence to isPresent
|
||||
*/
|
||||
public async setPresence(isPresent: boolean): Promise<void> {
|
||||
await this.http.post(environment.urlPrefix + '/users/setpresence/', isPresent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a default WhoAmI response
|
||||
*/
|
||||
private getDefaultWhoAmIResponse(): WhoAmI {
|
||||
return {
|
||||
user_id: null,
|
||||
guest_enabled: false,
|
||||
user: null,
|
||||
auth_type: DEFAULT_AUTH_TYPE,
|
||||
permissions: []
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from '../../../e2e-imports.module';
|
||||
import { ProjectorDataService } from './projector-data.service';
|
||||
|
||||
describe('ProjectorDataService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [ProjectorDataService]
|
||||
});
|
||||
});
|
||||
it('should be created', inject([ProjectorDataService], (service: ProjectorDataService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,179 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { BehaviorSubject, Observable, Subject } from 'rxjs';
|
||||
import { auditTime } from 'rxjs/operators';
|
||||
|
||||
import { Projector, ProjectorElement } from 'app/shared/models/core/projector';
|
||||
import { CommunicationManagerService, OfflineError } from './communication-manager.service';
|
||||
|
||||
export interface SlideData<T = { error?: string }, P extends ProjectorElement = ProjectorElement> {
|
||||
data: T;
|
||||
element: P;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type ProjectorData = SlideData[];
|
||||
|
||||
interface AllProjectorData {
|
||||
[id: number]: ProjectorData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Received data from server.
|
||||
*/
|
||||
interface ProjectorDataMessage {
|
||||
/**
|
||||
* The `change_id` of the current update.
|
||||
*/
|
||||
change_id: number;
|
||||
|
||||
/**
|
||||
* The necessary new projector-data.
|
||||
*/
|
||||
data: AllProjectorData;
|
||||
}
|
||||
|
||||
/**
|
||||
* This service handles the websocket connection for the projector data.
|
||||
* Each projector instance registers itself by calling `getProjectorObservable`.
|
||||
* A projector should deregister itself, when the component is destroyed.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ProjectorDataService {
|
||||
/**
|
||||
* Counts the open projector instances per projector id.
|
||||
*/
|
||||
private openProjectorInstances: { [id: number]: number } = {};
|
||||
|
||||
/**
|
||||
* Holds the current projector data for each projector.
|
||||
*/
|
||||
private currentProjectorData: { [id: number]: BehaviorSubject<ProjectorData | null> } = {};
|
||||
|
||||
/**
|
||||
* When multiple projectory are requested, debounce these requests to just issue
|
||||
* one request, with all the needed projectors.
|
||||
*/
|
||||
private readonly updateProjectorDataDebounceSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* Holds the current change id to check, if the update contains new content or a deprecated one.
|
||||
*/
|
||||
private currentChangeId = 0;
|
||||
|
||||
private streamCloseFn: () => void | null = null;
|
||||
|
||||
public constructor(private communicationManager: CommunicationManagerService) {
|
||||
this.communicationManager.startCommunicationEvent.subscribe(() => this.updateProjectorDataSubscription());
|
||||
|
||||
// With a bit of debounce, update the needed projectors.
|
||||
this.updateProjectorDataDebounceSubject.pipe(auditTime(10)).subscribe(() => {
|
||||
const allActiveProjectorIds = Object.keys(this.openProjectorInstances)
|
||||
.map(id => parseInt(id, 10))
|
||||
.filter(id => this.openProjectorInstances[id] > 0);
|
||||
this.requestProjectors(allActiveProjectorIds);
|
||||
});
|
||||
}
|
||||
|
||||
public async requestProjectors(allActiveProjectorIds: number[]): Promise<void> {
|
||||
this.cancelCurrentServerSubscription();
|
||||
|
||||
if (allActiveProjectorIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.streamCloseFn = await this.communicationManager.subscribe<ProjectorDataMessage>(
|
||||
'/system/projector',
|
||||
message => {
|
||||
this.handleMesage(message);
|
||||
},
|
||||
() => ({ projector_ids: allActiveProjectorIds.join(',') })
|
||||
);
|
||||
} catch (e) {
|
||||
if (!(e instanceof OfflineError)) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public cancelCurrentServerSubscription(): void {
|
||||
if (this.streamCloseFn) {
|
||||
this.streamCloseFn();
|
||||
this.streamCloseFn = null;
|
||||
}
|
||||
}
|
||||
|
||||
private handleMesage(message: ProjectorDataMessage): void {
|
||||
if (this.currentChangeId > message.change_id) {
|
||||
console.log('Projector: Change id too low:', this.currentChangeId, message.change_id);
|
||||
return;
|
||||
}
|
||||
Object.keys(message.data).forEach(_id => {
|
||||
const id = parseInt(_id, 10);
|
||||
if (this.currentProjectorData[id]) {
|
||||
this.currentProjectorData[id].next(message.data[id] as ProjectorData);
|
||||
}
|
||||
});
|
||||
this.currentChangeId = message.change_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an observable for the projector data.
|
||||
*
|
||||
* @param projectorId The requested projector
|
||||
* @return an observable for the projector data of the given projector.
|
||||
*/
|
||||
public getProjectorObservable(projectorId: number): Observable<ProjectorData | null> {
|
||||
// Count projectors.
|
||||
if (!this.openProjectorInstances[projectorId]) {
|
||||
this.openProjectorInstances[projectorId] = 1;
|
||||
if (!this.currentProjectorData[projectorId]) {
|
||||
this.currentProjectorData[projectorId] = new BehaviorSubject<ProjectorData | null>(null);
|
||||
}
|
||||
} else {
|
||||
this.openProjectorInstances[projectorId]++;
|
||||
}
|
||||
|
||||
// Projector opened the first time.
|
||||
if (this.openProjectorInstances[projectorId] === 1) {
|
||||
this.updateProjectorDataSubscription();
|
||||
}
|
||||
return this.currentProjectorData[projectorId].asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe data from the server, if the last projector was closed.
|
||||
*
|
||||
* @param projectorId the projector.
|
||||
*/
|
||||
public projectorClosed(projectorId: number): void {
|
||||
if (this.openProjectorInstances[projectorId]) {
|
||||
this.openProjectorInstances[projectorId]--;
|
||||
}
|
||||
if (this.openProjectorInstances[projectorId] === 0) {
|
||||
this.updateProjectorDataSubscription();
|
||||
this.currentProjectorData[projectorId].next(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests to update the data subscription to the server.
|
||||
*/
|
||||
private updateProjectorDataSubscription(): void {
|
||||
this.updateProjectorDataDebounceSubject.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the available projectior data for the given projector. Note that the data
|
||||
* might not be there, if there is no subscribtion for this projector. But the
|
||||
* data, if exist, is always the current data.
|
||||
*/
|
||||
public getAvailableProjectorData(projector: Projector): ProjectorData | null {
|
||||
if (this.currentProjectorData[projector.id]) {
|
||||
return this.currentProjectorData[projector.id].getValue();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { ProjectorService } from './projector.service';
|
||||
|
||||
describe('ProjectorService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [ProjectorService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([ProjectorService], (service: ProjectorService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,430 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { BaseModel } from 'app/shared/models/base/base-model';
|
||||
import { ProjectionDefault } from 'app/shared/models/core/projection-default';
|
||||
import {
|
||||
elementIdentifies,
|
||||
IdentifiableProjectorElement,
|
||||
Projector,
|
||||
ProjectorElement,
|
||||
ProjectorElements
|
||||
} from 'app/shared/models/core/projector';
|
||||
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
|
||||
import {
|
||||
isProjectable,
|
||||
isProjectorElementBuildDeskriptor,
|
||||
Projectable,
|
||||
ProjectorElementBuildDeskriptor
|
||||
} from 'app/site/base/projectable';
|
||||
import { SlideManager } from 'app/slides/services/slide-manager.service';
|
||||
import { ConfigService } from '../ui-services/config.service';
|
||||
import { DataStoreService } from './data-store.service';
|
||||
import { HttpService } from './http.service';
|
||||
import { ProjectorDataService } from './projector-data.service';
|
||||
import { ViewModelStoreService } from './view-model-store.service';
|
||||
|
||||
export interface ProjectorTitle {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This service cares about Projectables being projected and manage all projection-related
|
||||
* actions.
|
||||
*
|
||||
* We cannot access the ProjectorRepository here, so we will deal with plain projector objects.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ProjectorService {
|
||||
public constructor(
|
||||
private DS: DataStoreService,
|
||||
private http: HttpService,
|
||||
private slideManager: SlideManager,
|
||||
private viewModelStore: ViewModelStoreService,
|
||||
private translate: TranslateService,
|
||||
private configService: ConfigService,
|
||||
private projectorDataService: ProjectorDataService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retusn the identifiable projector element from the given types of slides/elements/descriptors
|
||||
*
|
||||
* @param obj Something related to IdentifiableProjectorElement
|
||||
* @returns the identifiable projector element from obj.
|
||||
*/
|
||||
private getProjectorElement(
|
||||
obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement
|
||||
): IdentifiableProjectorElement {
|
||||
if (isProjectable(obj)) {
|
||||
return obj.getSlide(this.configService).getBasicProjectorElement({});
|
||||
} else if (isProjectorElementBuildDeskriptor(obj)) {
|
||||
return obj.getBasicProjectorElement({});
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, if a given object is projected.
|
||||
*
|
||||
* @param obj The object in question
|
||||
* @returns true, if the object is projected on one projector.
|
||||
*/
|
||||
public isProjected(obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement): boolean {
|
||||
const element = this.getProjectorElement(obj);
|
||||
if (element?.getIdentifiers) {
|
||||
return this.DS.getAll<Projector>('core/projector').some(projector => {
|
||||
return projector.isElementShown(element);
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all projectors where the object is prejected on.
|
||||
*
|
||||
* @param obj The object in question
|
||||
* @return All projectors, where this Object is projected on
|
||||
*/
|
||||
public getProjectorsWhichAreProjecting(
|
||||
obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement
|
||||
): Projector[] {
|
||||
const element = this.getProjectorElement(obj);
|
||||
return this.DS.getAll<Projector>('core/projector').filter(projector => {
|
||||
return projector.isElementShown(element);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, if the object is projected on the given projector.
|
||||
*
|
||||
* @param obj The object
|
||||
* @param projector The projector to test
|
||||
* @returns true, if the object is projected on the projector.
|
||||
*/
|
||||
public isProjectedOn(
|
||||
obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement,
|
||||
projector: Projector
|
||||
): boolean {
|
||||
return projector.isElementShown(this.getProjectorElement(obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* Projects the given ProjectorElement on the given projectors. Removes the element
|
||||
* from all non-given projectors
|
||||
*
|
||||
* @param projectors All projectors where to add the element.
|
||||
* @param element The element in question.
|
||||
*/
|
||||
public projectOnMultiple(projectors: Projector[], element: IdentifiableProjectorElement): void {
|
||||
this.DS.getAll<Projector>('core/projector').forEach(projector => {
|
||||
if (projectors.includes(projector)) {
|
||||
this.projectOn(projector, element);
|
||||
} else if (projector.isElementShown(element)) {
|
||||
this.removeFrom(projector, element);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Projcets the given object on the projector. If the object is non-stable, all other non-stable
|
||||
* elements will be removed and added to the history.
|
||||
*
|
||||
* @param projector The projector to add the object to.
|
||||
* @param obj The object to project
|
||||
*/
|
||||
public async projectOn(
|
||||
projector: Projector,
|
||||
obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement
|
||||
): Promise<void> {
|
||||
const element = this.getProjectorElement(obj);
|
||||
|
||||
if (element.stable) {
|
||||
// remove the same element, if it is currently projected
|
||||
projector.removeElements(element);
|
||||
// Add this stable element
|
||||
projector.addElement(element);
|
||||
await this.projectRequest(projector, projector.elements);
|
||||
} else {
|
||||
// For non-stable elements remove all other non-stable elements, add them to the history and
|
||||
// add the one new element to the projector.
|
||||
const removedElements = projector.removeAllNonStableElements();
|
||||
let changed = removedElements.length > 0;
|
||||
|
||||
if (element) {
|
||||
projector.addElement(element);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
await this.projectRequest(projector, projector.elements, null, removedElements, false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given object from the projector. Non stable elements will be added to the history.
|
||||
*
|
||||
* @param projector The projector
|
||||
* @param obj the object to unproject
|
||||
*/
|
||||
public async removeFrom(
|
||||
projector: Projector,
|
||||
obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement
|
||||
): Promise<void> {
|
||||
const element = this.getProjectorElement(obj);
|
||||
|
||||
const removedElements = projector.removeElements(element);
|
||||
if (removedElements.length > 0) {
|
||||
if (element.stable) {
|
||||
await this.projectRequest(projector, projector.elements);
|
||||
} else {
|
||||
// For non-stable elements: Add removed elements to the history.
|
||||
await this.projectRequest(projector, projector.elements, null, removedElements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async updateElement(
|
||||
projector: Projector,
|
||||
obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement
|
||||
): Promise<void> {
|
||||
const element = this.getProjectorElement(obj);
|
||||
projector.replaceElements(element);
|
||||
await this.projectRequest(projector, projector.elements, projector.elements_preview);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the request to change projector elements.
|
||||
*
|
||||
* Note: Just one of `appendToHistory` and `deleteLastHistoryElement` can be given.
|
||||
*
|
||||
* @param projector The affected projector
|
||||
* @param elements (optional) Elements to set.
|
||||
* @param preview (optional) preview to set
|
||||
* @param appendToHistory (optional) Elements to be appended to the history
|
||||
* @param deleteLastHistroyElement (optional) If given, the last history element will be removed.
|
||||
*/
|
||||
private async projectRequest(
|
||||
projector: Projector,
|
||||
elements?: ProjectorElements,
|
||||
preview?: ProjectorElements,
|
||||
appendToHistory?: ProjectorElements,
|
||||
deleteLastHistroyElement?: boolean,
|
||||
resetScroll?: boolean
|
||||
): Promise<void> {
|
||||
const requestData: any = {};
|
||||
if (elements) {
|
||||
requestData.elements = this.cleanupElements(projector, elements);
|
||||
}
|
||||
if (preview) {
|
||||
requestData.preview = preview;
|
||||
}
|
||||
if (appendToHistory && appendToHistory.length) {
|
||||
requestData.append_to_history = appendToHistory;
|
||||
}
|
||||
if (deleteLastHistroyElement) {
|
||||
requestData.delete_last_history_element = true;
|
||||
}
|
||||
if (appendToHistory && appendToHistory.length && deleteLastHistroyElement) {
|
||||
throw new Error('You cannot append to the history and delete the last element at the same time');
|
||||
}
|
||||
if (resetScroll) {
|
||||
requestData.reset_scroll = resetScroll;
|
||||
}
|
||||
await this.http.post(`/rest/core/projector/${projector.id}/project/`, requestData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up stable elements with errors from the projector
|
||||
*
|
||||
* @param projector The projector
|
||||
* @param elements The elements to clean up
|
||||
* @reutns the cleaned up elements.
|
||||
*/
|
||||
private cleanupElements(projector: Projector, elements: ProjectorElements): ProjectorElements {
|
||||
const projectorData = this.projectorDataService.getAvailableProjectorData(projector);
|
||||
|
||||
if (projectorData) {
|
||||
projectorData.forEach(entry => {
|
||||
if (entry.data.error && entry.element.stable) {
|
||||
// Remove this element
|
||||
const idElementToRemove = this.slideManager.getIdentifiableProjectorElement(entry.element);
|
||||
elements = elements.filter(element => {
|
||||
return !elementIdentifies(idElementToRemove, element);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a projectiondefault, we want to retrieve the projector, that is assigned
|
||||
* to this default.
|
||||
*
|
||||
* @param projectiondefault The projection default
|
||||
* @return the projector associated to the given projectiondefault.
|
||||
*/
|
||||
public getProjectorForDefault(projectiondefault: string): Projector | null {
|
||||
const pd = this.DS.find(ProjectionDefault, _pd => _pd.name === projectiondefault);
|
||||
if (pd) {
|
||||
return this.DS.get<Projector>(Projector, pd.projector_id);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts, that the given element is mappable to a model or view model.
|
||||
* Throws an error, if this assertion fails.
|
||||
*
|
||||
* @param element The element to check
|
||||
*/
|
||||
private assertElementIsMappable(element: IdentifiableProjectorElement): void {
|
||||
if (!this.slideManager.canSlideBeMappedToModel(element.name)) {
|
||||
throw new Error('This projector element cannot be mapped to a model');
|
||||
}
|
||||
const identifiers = element.getIdentifiers();
|
||||
if (!identifiers.includes('name') || !identifiers.includes('id')) {
|
||||
throw new Error('To map this element to a model, a name and id is needed.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a model associated with the identifiable projector element. Throws an error,
|
||||
* if the element is not mappable.
|
||||
*
|
||||
* @param element The projector element
|
||||
* @returns the model from the projector element
|
||||
*/
|
||||
public getModelFromProjectorElement<T extends BaseModel>(element: IdentifiableProjectorElement): T {
|
||||
this.assertElementIsMappable(element);
|
||||
return this.DS.get<T>(element.name, element.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a view model associated with the identifiable projector element. Throws an error,
|
||||
* if the element is not mappable.
|
||||
*
|
||||
* @param element The projector element
|
||||
* @returns the view model from the projector element
|
||||
*/
|
||||
public getViewModelFromIdentifiableProjectorElement<T extends BaseProjectableViewModel>(
|
||||
element: IdentifiableProjectorElement
|
||||
): T {
|
||||
this.assertElementIsMappable(element);
|
||||
const viewModel = this.viewModelStore.get<T>(element.name, element.id);
|
||||
if (viewModel && !isProjectable(viewModel)) {
|
||||
console.error('The view model is not projectable', viewModel, element);
|
||||
}
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
public getViewModelFromProjectorElement<T extends BaseProjectableViewModel>(element: ProjectorElement): T {
|
||||
const idElement = this.slideManager.getIdentifiableProjectorElement(element);
|
||||
return this.getViewModelFromIdentifiableProjectorElement(idElement);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public getSlideTitle(element: ProjectorElement): ProjectorTitle {
|
||||
if (this.slideManager.canSlideBeMappedToModel(element.name)) {
|
||||
const viewModel = this.getViewModelFromProjectorElement(element);
|
||||
if (viewModel) {
|
||||
return viewModel.getProjectorTitle();
|
||||
}
|
||||
}
|
||||
const configuration = this.slideManager.getSlideConfiguration(element.name);
|
||||
if (configuration.getSlideTitle) {
|
||||
return configuration.getSlideTitle(element, this.translate, this.viewModelStore);
|
||||
}
|
||||
|
||||
return { title: this.translate.instant(this.slideManager.getSlideVerboseName(element.name)) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Projects the next slide in the queue. Moves all currently projected
|
||||
* non-stable slides to the history.
|
||||
*
|
||||
* @param projector The projector
|
||||
*/
|
||||
public async projectNextSlide(projector: Projector): Promise<void> {
|
||||
await this.projectPreviewSlide(projector, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Projects one slide (given by the index of the preview) on the given projector. Moves
|
||||
* all current projected non-stable elements to the history.
|
||||
*
|
||||
* @param projector The projector
|
||||
* @param previewIndex The index in the `elements_preview` array.
|
||||
*/
|
||||
public async projectPreviewSlide(projector: Projector, previewIndex: number): Promise<void> {
|
||||
if (projector.elements_preview.length === 0 || previewIndex >= projector.elements_preview.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const removedElements = projector.removeAllNonStableElements();
|
||||
projector.addElement(projector.elements_preview.splice(previewIndex, 1)[0]);
|
||||
await this.projectRequest(
|
||||
projector,
|
||||
projector.elements,
|
||||
projector.elements_preview,
|
||||
removedElements,
|
||||
false,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Projects the last slide of the history. This slide will be removed from the history.
|
||||
*
|
||||
* @param projector The projector
|
||||
*/
|
||||
public async projectPreviousSlide(projector: Projector): Promise<void> {
|
||||
if (projector.elements_history.length === 0) {
|
||||
return;
|
||||
}
|
||||
// Get the last element from the history
|
||||
const lastElements: ProjectorElements = projector.elements_history[projector.elements_history.length - 1];
|
||||
let lastElement: ProjectorElement = null;
|
||||
if (lastElements.length > 0) {
|
||||
lastElement = lastElements[0];
|
||||
}
|
||||
|
||||
// Add all current elements to the preview.
|
||||
const removedElements = projector.removeAllNonStableElements();
|
||||
removedElements.forEach(e => projector.elements_preview.unshift(e));
|
||||
|
||||
// Add last element
|
||||
if (lastElement) {
|
||||
projector.addElement(lastElement);
|
||||
}
|
||||
await this.projectRequest(projector, projector.elements, projector.elements_preview, null, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the preview of the projector
|
||||
*
|
||||
* @param projector The projector to save the preview.
|
||||
*/
|
||||
public async savePreview(projector: Projector): Promise<void> {
|
||||
await this.projectRequest(projector, null, projector.elements_preview);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the given element to the preview.
|
||||
*
|
||||
* @param projector The projector
|
||||
* @param element The element to add to the preview.
|
||||
*/
|
||||
public async addElementToPreview(projector: Projector, element: ProjectorElement): Promise<void> {
|
||||
projector.elements_preview.push(element);
|
||||
await this.projectRequest(projector, null, projector.elements_preview);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { inject, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
import { PwaService } from './pwa.service';
|
||||
|
||||
describe('PwaService', () => {
|
||||
beforeEach(() =>
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [PwaService]
|
||||
})
|
||||
);
|
||||
|
||||
it('should be created', inject([PwaService], (service: PwaService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { SwUpdate } from '@angular/service-worker';
|
||||
|
||||
/**
|
||||
* Service for Progressive Web App options
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class PwaService {
|
||||
public promptEvent;
|
||||
|
||||
public constructor(swUpdate: SwUpdate) {
|
||||
// check if an update is available
|
||||
swUpdate.available.subscribe(event => {
|
||||
// TODO: ask user if app should update now
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
// install button
|
||||
window.addEventListener('beforeinstallprompt', event => {
|
||||
this.promptEvent = event;
|
||||
});
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue