Compare commits
412 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 |
8
.gitignore
vendored
8
.gitignore
vendored
@ -13,6 +13,12 @@
|
|||||||
node_modules/*
|
node_modules/*
|
||||||
bower_components/*
|
bower_components/*
|
||||||
|
|
||||||
|
# OS4-Submodules
|
||||||
|
/openslides-*
|
||||||
|
|
||||||
|
# OS3+
|
||||||
|
/server/
|
||||||
|
|
||||||
# Local user data (settings, database, media, search index, static files)
|
# Local user data (settings, database, media, search index, static files)
|
||||||
personal_data/*
|
personal_data/*
|
||||||
openslides/static/*
|
openslides/static/*
|
||||||
@ -26,6 +32,7 @@ dist/*
|
|||||||
debug/*
|
debug/*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea
|
.idea
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
# Unit test and coverage reports
|
# Unit test and coverage reports
|
||||||
.coverage
|
.coverage
|
||||||
@ -77,6 +84,7 @@ client/yarn.lock
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
client/package-lock.json
|
client/package-lock.json
|
||||||
cypress.json
|
cypress.json
|
||||||
|
*-version.txt
|
||||||
|
|
||||||
# System Files
|
# System Files
|
||||||
client/.DS_Store
|
client/.DS_Store
|
||||||
|
21
.travis.yml
21
.travis.yml
@ -25,7 +25,7 @@ matrix:
|
|||||||
|
|
||||||
- name: "Installing npm dependencies"
|
- name: "Installing npm dependencies"
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js: "10.9"
|
node_js: "12.18"
|
||||||
cache:
|
cache:
|
||||||
- directories:
|
- directories:
|
||||||
- "client/node_modules"
|
- "client/node_modules"
|
||||||
@ -39,7 +39,7 @@ matrix:
|
|||||||
- stage: "Run tests"
|
- stage: "Run tests"
|
||||||
name: "Client: Testing"
|
name: "Client: Testing"
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js: "10.9"
|
node_js: "12.18"
|
||||||
apt:
|
apt:
|
||||||
sources:
|
sources:
|
||||||
- google-chrome
|
- google-chrome
|
||||||
@ -56,7 +56,7 @@ matrix:
|
|||||||
|
|
||||||
- name: "Client: Production Build (ES5)"
|
- name: "Client: Production Build (ES5)"
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js: "10.9"
|
node_js: "12.18"
|
||||||
install:
|
install:
|
||||||
- cd client
|
- cd client
|
||||||
- sed -i '/\"target\"/c\\"target\":\"es5\",' tsconfig.json
|
- sed -i '/\"target\"/c\\"target\":\"es5\",' tsconfig.json
|
||||||
@ -65,7 +65,7 @@ matrix:
|
|||||||
|
|
||||||
- name: "Client: Production Build (ES2015)"
|
- name: "Client: Production Build (ES2015)"
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js: "10.9"
|
node_js: "12.18"
|
||||||
install:
|
install:
|
||||||
- cd client
|
- cd client
|
||||||
- echo "Firefox ESR" > browserslist
|
- echo "Firefox ESR" > browserslist
|
||||||
@ -74,7 +74,7 @@ matrix:
|
|||||||
|
|
||||||
- name: "Client: Build"
|
- name: "Client: Build"
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js: "10.9"
|
node_js: "12.18"
|
||||||
script:
|
script:
|
||||||
- cd client
|
- cd client
|
||||||
- npm run build-debug
|
- npm run build-debug
|
||||||
@ -85,7 +85,7 @@ matrix:
|
|||||||
- "3.6"
|
- "3.6"
|
||||||
script:
|
script:
|
||||||
- mypy openslides/ tests/
|
- mypy openslides/ tests/
|
||||||
- pytest --cov --cov-fail-under=73
|
- pytest --cov --cov-fail-under=75
|
||||||
|
|
||||||
- name: "Server: Tests Python 3.7"
|
- name: "Server: Tests Python 3.7"
|
||||||
language: python
|
language: python
|
||||||
@ -96,7 +96,7 @@ matrix:
|
|||||||
- isort --check-only --diff --recursive openslides tests
|
- isort --check-only --diff --recursive openslides tests
|
||||||
- black --check --diff --target-version py36 openslides tests
|
- black --check --diff --target-version py36 openslides tests
|
||||||
- mypy openslides/ tests/
|
- mypy openslides/ tests/
|
||||||
- pytest --cov --cov-fail-under=73
|
- pytest --cov --cov-fail-under=75
|
||||||
|
|
||||||
- name: "Server: Tests Python 3.8"
|
- name: "Server: Tests Python 3.8"
|
||||||
language: python
|
language: python
|
||||||
@ -107,21 +107,20 @@ matrix:
|
|||||||
- isort --check-only --diff --recursive openslides tests
|
- isort --check-only --diff --recursive openslides tests
|
||||||
- black --check --diff --target-version py36 openslides tests
|
- black --check --diff --target-version py36 openslides tests
|
||||||
- mypy openslides/ tests/
|
- mypy openslides/ tests/
|
||||||
- pytest --cov --cov-fail-under=73
|
- pytest --cov --cov-fail-under=75
|
||||||
|
|
||||||
- name: "Client: Linting"
|
- name: "Client: Linting"
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js: "10.9"
|
node_js: "12.18"
|
||||||
script:
|
script:
|
||||||
- cd client
|
- cd client
|
||||||
- npm run lint-check
|
- npm run lint-check
|
||||||
|
|
||||||
- name: "Client: Code Formatting Check"
|
- name: "Client: Code Formatting Check"
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js: "10.9"
|
node_js: "12.18"
|
||||||
script:
|
script:
|
||||||
- cd client
|
- cd client
|
||||||
- npm list --depth=0 || cat --help
|
|
||||||
- npm run prettify-check
|
- npm run prettify-check
|
||||||
|
|
||||||
- name: "Server: Tests Startup Routine Python 3.7"
|
- name: "Server: Tests Startup Routine Python 3.7"
|
||||||
|
@ -4,9 +4,79 @@
|
|||||||
|
|
||||||
https://openslides.com
|
https://openslides.com
|
||||||
|
|
||||||
|
Version 3.2 (2020-07-15)
|
||||||
|
========================
|
||||||
|
`Milestone <https://github.com/OpenSlides/OpenSlides/milestones/3.2>`_
|
||||||
|
|
||||||
|
General:
|
||||||
|
- New electronic voting integrated for motions and elections [#5255].
|
||||||
|
- New WebRTC based voice and video conferences using Jitsi-Meet (requires external Jitsi-Meet Server) [#5309, #5371, #5394, #5430, #5437, #5442, #5452, #5453].
|
||||||
|
- Improved system libraries (upgraded to Angular 9 which uses the Ivy rendering engine) [#5234].
|
||||||
|
- Improved the load of autoupdate system [#5109, #5375].
|
||||||
|
- Improved relations (i.e discovery of Motion - User - Motions). [#5091, #5180, #5389].
|
||||||
|
- Improved server validation of HTML in the OpenSlides config [#5168].
|
||||||
|
- Improved UI, UX, stability and theming [#5228, #5238, #5262, #5270, #5272, #5274, #5278, #5410, #5429].
|
||||||
|
- Improved themes (new: default dark, red light, green dark and solarized) and better support for dark themes [#5416, #5431, #5451].
|
||||||
|
- Improved HTML validation for welcome page and agenda topics to allow more tags (e.g. div, video) and attributes/styles [#5314].
|
||||||
|
- Improved permission checking system in client [#5359].
|
||||||
|
- Improved browser support by catching unsuported browsers on login page [#5403, #5446].
|
||||||
|
- Improved SAML support [#5405, #5418, #5432].
|
||||||
|
- Fixed wrong relative urls in TinyMCE [#5349].
|
||||||
|
- Fixed PDF generation if a left footer image was set [#5443].
|
||||||
|
- Removed the "check update for other clients" button [#5277].
|
||||||
|
- Various cleanups and improvements to usability, performance and translation.
|
||||||
|
|
||||||
|
Agenda:
|
||||||
|
- New tags for agenda items [#5370].
|
||||||
|
- New possibility to duplicate selected topic items [#5433].
|
||||||
|
- New 'create user' button in list of speakers if user was not found in the search box [#5307].
|
||||||
|
- New list of speakers statistic section on legal notice page [#5347].
|
||||||
|
- New "first contribution" hint for speakers [#5330].
|
||||||
|
- Improved showing comments in agenda list (as separate line) and projector queue [#5293].
|
||||||
|
- Improved line height of agenda slide [#5419].
|
||||||
|
- Fixed agenda PDF where the agenda item number was printed twice [#5417, #5454].
|
||||||
|
- Fixed negative speakers duration [#5447, #5448].
|
||||||
|
|
||||||
|
Motions:
|
||||||
|
- New electronic voting feature for motions [#5255].
|
||||||
|
- New possibility to create paragraph based amendments of paragraph based amendments [#5173].
|
||||||
|
- New option for page breaks in motion PDF export [#5191].
|
||||||
|
- New option to show all changes of amendments in main motion (clientside) [#5348].
|
||||||
|
- New 'done' indicator for motion block if all motions reached their final state [#5246].
|
||||||
|
- Improved PDF table of content (hide the recommendation if state is final) [#5192].
|
||||||
|
- Improved creating "final print version" (modified final version) also for motions without change recommendations [#5193, #5209].
|
||||||
|
- Improved voting results with nice charts.
|
||||||
|
- Improved navigation between amendments (reflects sorting of amendment list if the option "show amendments together with motions" is disabled) [#5245].
|
||||||
|
- Improved workflow manager for small devices [#5280].
|
||||||
|
- Improved sorting motions by category (sorts the list by category weight instead of the identifier) [#5308, #5310].
|
||||||
|
- Improved preselection and fallback behavior for motions with various change recommendation settings [#5366]
|
||||||
|
- Improved CSV/XLSX export (moved motion id as last column) for easier import via CSV [#5425].
|
||||||
|
- Fixed error by removing recommendation string in workflow manager [#5271].
|
||||||
|
- Fixed bug where TinyMCE changes would not update a motions save button [#5402].
|
||||||
|
|
||||||
|
Elections:
|
||||||
|
- New electronic voting feature for elections [#5255].
|
||||||
|
- Improved voting results with nice charts.
|
||||||
|
- Fixed some permission errors [#5194].
|
||||||
|
|
||||||
|
Users:
|
||||||
|
- New option to activate vote weight [#5305].
|
||||||
|
- New option to allow users to set themselves as present [#5283, #5317, #5319].
|
||||||
|
- Improved the permission "can see extra data" (only the fields email, comment, is_active, last_email_send are allowed) [#5423].
|
||||||
|
|
||||||
|
Mediafiles:
|
||||||
|
- External servers can be used to store media files [#5153, #5230].
|
||||||
|
|
||||||
|
Projector:
|
||||||
|
- New projector indicator for the currently projected element in all list view tables (visible for users without projector manage permission) [#5321].
|
||||||
|
- Improved "current list of speakers" reference for new projectors [#5273].
|
||||||
|
- Improved motion slide to hide submitter box if empty [#5367].
|
||||||
|
- New (configurable) monospace font for the countdown [#5378, #5408].
|
||||||
|
|
||||||
|
|
||||||
Version 3.1 (2019-12-13)
|
Version 3.1 (2019-12-13)
|
||||||
========================
|
========================
|
||||||
`Milestone <https://github.com/OpenSlides/OpenSlides/milestones/3.0>`_
|
`Milestone <https://github.com/OpenSlides/OpenSlides/milestones/3.1>`_
|
||||||
|
|
||||||
General:
|
General:
|
||||||
- Improved loading time of OpenSlides [#5061, 5087, #5110, #5146 - Breaks IE11].
|
- Improved loading time of OpenSlides [#5061, 5087, #5110, #5146 - Breaks IE11].
|
||||||
|
@ -268,6 +268,7 @@ This is an example ``nginx.conf`` configuration for Daphne listing on port
|
|||||||
proxy_pass http://localhost:8000;
|
proxy_pass http://localhost:8000;
|
||||||
}
|
}
|
||||||
location /rest {
|
location /rest {
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
proxy_pass http://localhost:8000;
|
proxy_pass http://localhost:8000;
|
||||||
}
|
}
|
||||||
location /ws {
|
location /ws {
|
||||||
|
@ -5,7 +5,7 @@ RUN mkdir /app
|
|||||||
RUN apt -y update && \
|
RUN apt -y update && \
|
||||||
apt -y upgrade && \
|
apt -y upgrade && \
|
||||||
apt install -y libpq-dev curl wget xz-utils bzip2 git gcc gnupg2 make g++
|
apt install -y libpq-dev curl wget xz-utils bzip2 git gcc gnupg2 make g++
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_11.x | bash -
|
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash -
|
||||||
RUN apt -y install nodejs
|
RUN apt -y install nodejs
|
||||||
RUN npm install -g @angular/cli@latest
|
RUN npm install -g @angular/cli@latest
|
||||||
RUN useradd -m openslides
|
RUN useradd -m openslides
|
||||||
|
25
SETTINGS.rst
25
SETTINGS.rst
@ -57,6 +57,25 @@ useful for debugging to print all email the the console::
|
|||||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||||
|
|
||||||
|
|
||||||
|
Electronic voting
|
||||||
|
=================
|
||||||
|
|
||||||
|
Electronic voting is disabled by default, so only analog polls are available.
|
||||||
|
To enable it, set::
|
||||||
|
|
||||||
|
ENABLE_ELECTRONIC_VOTING = True
|
||||||
|
|
||||||
|
|
||||||
|
Jitsi integration
|
||||||
|
=================
|
||||||
|
|
||||||
|
To enable the audio conference with Jitsi Meet, you have to set the following variables:
|
||||||
|
|
||||||
|
- `JITSI_DOMAIN`: must contain an url to a Jitsi server
|
||||||
|
- `JITSI_ROOM_NAME`: the name of the room that should be used
|
||||||
|
- `JITSI_ROOM_PASSWORD`: (optional) the password of the room. Will be applied automatically from the settings.
|
||||||
|
|
||||||
|
|
||||||
Logging
|
Logging
|
||||||
=======
|
=======
|
||||||
|
|
||||||
@ -66,7 +85,7 @@ We recommend to enable all OpenSlides related logging with level `INFO` per
|
|||||||
default::
|
default::
|
||||||
|
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
'formatters':
|
'formatters': {
|
||||||
'lessnoise': {
|
'lessnoise': {
|
||||||
'format': '[{levelname}] {name} {message}',
|
'format': '[{levelname}] {name} {message}',
|
||||||
'style': '{',
|
'style': '{',
|
||||||
@ -123,3 +142,7 @@ not affect the client.
|
|||||||
operator is in one of these groups, the client disconnected and reconnects again.
|
operator is in one of these groups, the client disconnected and reconnects again.
|
||||||
All requests urls (including websockets) are now prefixed with `/prioritize`, so
|
All requests urls (including websockets) are now prefixed with `/prioritize`, so
|
||||||
these requests from "prioritized clients" can be routed to different servers.
|
these requests from "prioritized clients" can be routed to different servers.
|
||||||
|
|
||||||
|
`AUTOUPDATE_DELAY`: The delay to send autoupdates. This feature can be
|
||||||
|
deactivated by setting it to `None`. It is deactivated per default. The Delay is
|
||||||
|
given in seconds
|
||||||
|
143
client/README.md
143
client/README.md
@ -59,76 +59,73 @@ Language files can be found in `/src/assets/i18n`.
|
|||||||
|
|
||||||
OpenSlides uses the following software or parts of them:
|
OpenSlides uses the following software or parts of them:
|
||||||
|
|
||||||
- [@angular/animations@8.2.4](https://github.com/angular/angular), License: MIT
|
- [@angular/animations@9.1.0](https://github.com/angular/angular), License: MIT
|
||||||
- [@angular/cdk-experimental@8.1.4](https://github.com/angular/components), License: MIT
|
- [@angular/cdk-experimental@9.2.0](https://github.com/angular/components), License: MIT
|
||||||
- [@angular/cdk@8.1.4](https://github.com/angular/components), License: MIT
|
- [@angular/cdk@9.2.0](https://github.com/angular/components), License: MIT
|
||||||
- [@angular/common@8.2.4](https://github.com/angular/angular), License: MIT
|
- [@angular/common@9.1.0](https://github.com/angular/angular), License: MIT
|
||||||
- [@angular/compiler@8.2.4](https://github.com/angular/angular), License: MIT
|
- [@angular/compiler@9.1.0](https://github.com/angular/angular), License: MIT
|
||||||
- [@angular/core@8.2.4](https://github.com/angular/angular), License: MIT
|
- [@angular/core@9.1.0](https://github.com/angular/angular), License: MIT
|
||||||
- [@angular/forms@8.2.4](https://github.com/angular/angular), License: MIT
|
- [@angular/forms@9.1.0](https://github.com/angular/angular), License: MIT
|
||||||
- [@angular/material-moment-adapter@8.1.4](https://github.com/angular/components), License: MIT
|
- [@angular/material-moment-adapter@9.2.0](https://github.com/angular/components), License: MIT
|
||||||
- [@angular/material@8.1.4](https://github.com/angular/components), License: MIT
|
- [@angular/material@9.2.0](https://github.com/angular/components), License: MIT
|
||||||
- [@angular/platform-browser-dynamic@8.2.4](https://github.com/angular/angular), License: MIT
|
- [@angular/platform-browser-dynamic@9.1.0](https://github.com/angular/angular), License: MIT
|
||||||
- [@angular/platform-browser@8.2.4](https://github.com/angular/angular), License: MIT
|
- [@angular/platform-browser@9.1.0](https://github.com/angular/angular), License: MIT
|
||||||
- [@angular/pwa@0.803.2](https://github.com/angular/angular-cli), License: MIT
|
- [@angular/router@9.1.0](https://github.com/angular/angular), License: MIT
|
||||||
- [@angular/router@8.2.4](https://github.com/angular/angular), License: MIT
|
- [@angular/service-worker@9.1.0](https://github.com/angular/angular), License: MIT
|
||||||
- [@angular/service-worker@8.2.4](https://github.com/angular/angular), License: MIT
|
- [@ngx-pwa/local-storage@9.0.3](https://github.com/cyrilletuzi/angular-async-local-storage), License: MIT
|
||||||
- [@ngx-pwa/local-storage@8.2.1](https://github.com/cyrilletuzi/angular-async-local-storage), License: MIT
|
- [@ngx-translate/core@12.1.2](https://github.com/ngx-translate/core), License: MIT
|
||||||
- [@ngx-translate/core@11.0.1](https://github.com/ngx-translate/core), License: MIT
|
- [@ngx-translate/http-loader@4.0.0](https://github.com/ngx-translate/http-loader), License: MIT
|
||||||
- [@ngx-translate/http-loader@4.0.0](https://github.com/ngx-translate/http-loader), License: MIT
|
- [@pebula/ngrid-material@2.0.0-rc.1](undefined), License: MIT
|
||||||
- [@pebula/ngrid-material@1.0.0-rc.5](https://github.com/shlomiassaf/ngrid), License: MIT
|
- [@pebula/ngrid@2.0.0-rc.1](https://github.com/shlomiassaf/ngrid), License: MIT
|
||||||
- [@pebula/ngrid@1.0.0-rc.5](https://github.com/shlomiassaf/ngrid), License: MIT
|
- [@pebula/utils@1.0.2](undefined), License: MIT
|
||||||
- [@pebula/utils@1.0.0](https://github.com/shlomiassaf/ngrid), License: MIT
|
- [@tinymce/tinymce-angular@3.5.0](https://github.com/tinymce/tinymce-angular), License: Apache-2.0
|
||||||
- [@tinymce/tinymce-angular@3.3.0](https://github.com/tinymce/tinymce-angular), License: Apache-2.0
|
- [acorn@7.1.1](https://github.com/acornjs/acorn), License: MIT
|
||||||
- [acorn@7.0.0](https://github.com/acornjs/acorn), License: MIT
|
- [chart.js@2.9.3](https://github.com/chartjs/Chart.js), License: MIT
|
||||||
- [core-js@3.2.1](https://github.com/zloirock/core-js), License: MIT
|
- [core-js@3.6.4](https://github.com/zloirock/core-js), License: MIT
|
||||||
- [css-element-queries@1.2.1](https://github.com/marcj/css-element-queries), License: MIT
|
- [css-element-queries@1.2.3](https://github.com/marcj/css-element-queries), License: MIT
|
||||||
- [exceljs@1.15.0](https://github.com/exceljs/exceljs), License: MIT
|
- [exceljs@3.8.2](https://github.com/exceljs/exceljs), License: MIT
|
||||||
- [file-saver@2.0.2](https://github.com/eligrey/FileSaver.js), License: MIT
|
- [file-saver@2.0.2](https://github.com/eligrey/FileSaver.js), License: MIT
|
||||||
- [hammerjs@2.0.8](https://github.com/hammerjs/hammer.js), License: MIT
|
- [lz4js@0.2.0](https://github.com/Benzinga/lz4js), License: ISC
|
||||||
- [lz4js@0.2.0](https://github.com/Benzinga/lz4js), License: ISC
|
- [material-icon-font@0.1.0](https://github.com//petergng/svgFontCreator), License: ISC
|
||||||
- [material-icon-font@0.1.0](https://github.com//petergng/svgFontCreator), License: ISC
|
- [moment@2.24.0](https://github.com/moment/moment), License: MIT
|
||||||
- [moment@2.24.0](https://github.com/moment/moment), License: MIT
|
- [ng2-charts@2.3.0](https://github.com/valor-software/ng2-charts), License: ISC
|
||||||
- [ng2-pdf-viewer@5.3.4](git+https://vadimdez@github.com/VadimDez/ng2-pdf-viewer), License: MIT
|
- [ng2-pdf-viewer@6.1.2](git+https://vadimdez@github.com/VadimDez/ng2-pdf-viewer), License: MIT
|
||||||
- [ngx-file-drop@8.0.7](https://github.com/georgipeltekov/ngx-file-drop), License: MIT
|
- [ngx-file-drop@8.0.8](https://github.com/georgipeltekov/ngx-file-drop), License: MIT
|
||||||
- [ngx-mat-select-search@1.8.0](https://github.com/bithost-gmbh/ngx-mat-select-search), License: MIT
|
- [ngx-mat-select-search@2.1.2](https://github.com/bithost-gmbh/ngx-mat-select-search), License: MIT
|
||||||
- [ngx-material-timepicker@4.0.2](https://github.com/Agranom/ngx-material-timepicker), License: MIT
|
- [ngx-material-timepicker@5.5.1](https://github.com/Agranom/ngx-material-timepicker), License: MIT
|
||||||
- [ngx-papaparse@4.0.2](https://github.com/alberthaff/ngx-papaparse), License: MIT
|
- [ngx-papaparse@4.0.4](https://github.com/alberthaff/ngx-papaparse), License: MIT
|
||||||
- [pdfmake@0.1.58](https://github.com/bpampuch/pdfmake), License: MIT
|
- [pdfmake@0.1.65](https://github.com/bpampuch/pdfmake), License: MIT
|
||||||
- [po2json@1.0.0-alpha](https://github.com/mikeedwards/po2json), License: GNU Library General Public License
|
- [po2json@1.0.0-beta-2](https://github.com/mikeedwards/po2json), License: LGPL-2.0-or-later
|
||||||
- [rxjs@6.5.2](https://github.com/reactivex/rxjs), License: Apache-2.0
|
- [rxjs@6.5.4](https://github.com/reactivex/rxjs), License: Apache-2.0
|
||||||
- [text-encoding@0.7.0](https://github.com/inexorabletash/text-encoding), License: (Unlicense OR Apache-2.0)
|
- [tinymce@5.2.1](https://github.com/tinymce/tinymce-dist), License: LGPL-2.1
|
||||||
- [tinymce@5.0.14](https://github.com/tinymce/tinymce-dist), License: LGPL-2.1
|
- [tslib@1.11.1](https://github.com/Microsoft/tslib), License: Apache-2.0
|
||||||
- [tslib@1.10.0](https://github.com/Microsoft/tslib), License: Apache-2.0
|
- [zone.js@0.10.3](https://github.com/angular/angular), License: MIT
|
||||||
- [uuid@3.3.3](https://github.com/kelektiv/node-uuid), License: MIT
|
- [@angular-devkit/build-angular@0.901.0](https://github.com/angular/angular-cli), License: MIT
|
||||||
- [zone.js@0.9.1](https://github.com/angular/zone.js), License: MIT
|
- [@angular-devkit/schematics@9.1.0](https://github.com/angular/angular-cli), License: MIT
|
||||||
- [@angular-devkit/build-angular@0.803.2](https://github.com/angular/angular-cli), License: MIT
|
- [@angular/cli@9.1.0](https://github.com/angular/angular-cli), License: MIT
|
||||||
- [@angular/cli@8.3.2](https://github.com/angular/angular-cli), License: MIT
|
- [@angular/compiler-cli@9.1.0](https://github.com/angular/angular), License: MIT
|
||||||
- [@angular/compiler-cli@8.2.4](https://github.com/angular/angular), License: MIT
|
- [@angular/language-service@9.1.0](https://github.com/angular/angular), License: MIT
|
||||||
- [@angular/language-service@8.2.4](https://github.com/angular/angular), License: MIT
|
- [@biesbjerg/ngx-translate-extract@6.0.3](https://github.com/biesbjerg/ngx-translate-extract), License: MIT
|
||||||
- [@biesbjerg/ngx-translate-extract@3.0.5](https://github.com/biesbjerg/ngx-translate-extract), License: MIT
|
- [@compodoc/compodoc@1.1.11](https://github.com/compodoc/compodoc), License: MIT
|
||||||
- [@compodoc/compodoc@1.1.10](https://github.com/compodoc/compodoc), License: MIT
|
- [@schematics/angular@9.1.0](https://github.com/angular/angular-cli), License: MIT
|
||||||
- [@types/jasmine@3.4.0](https://github.com/DefinitelyTyped/DefinitelyTyped), License: MIT
|
- [@types/jasmine@3.5.10](https://github.com/DefinitelyTyped/DefinitelyTyped), License: MIT
|
||||||
- [@types/jasminewd2@2.0.6](https://github.com/DefinitelyTyped/DefinitelyTyped), License: MIT
|
- [@types/jasminewd2@2.0.8](https://github.com/DefinitelyTyped/DefinitelyTyped), License: MIT
|
||||||
- [@types/node@12.7.3](https://github.com/DefinitelyTyped/DefinitelyTyped), License: MIT
|
- [@types/node@13.9.8](https://github.com/DefinitelyTyped/DefinitelyTyped), License: MIT
|
||||||
- [@types/yargs@13.0.2](https://github.com/DefinitelyTyped/DefinitelyTyped), License: MIT
|
- [@types/yargs@15.0.4](https://github.com/DefinitelyTyped/DefinitelyTyped), License: MIT
|
||||||
- [codelyzer@5.1.0](https://github.com/mgechev/codelyzer), License: MIT
|
- [codelyzer@5.2.2](https://github.com/mgechev/codelyzer), License: MIT
|
||||||
- [husky@3.0.4](https://github.com/typicode/husky), License: MIT
|
- [husky@4.2.3](https://github.com/typicode/husky), License: MIT
|
||||||
- [jasmine-core@3.4.0](https://github.com/jasmine/jasmine), License: MIT
|
- [jasmine-core@3.5.0](https://github.com/jasmine/jasmine), License: MIT
|
||||||
- [jasmine-spec-reporter@4.2.1](https://github.com/bcaudan/jasmine-spec-reporter), License: Apache-2.0
|
- [jasmine-spec-reporter@5.0.1](https://github.com/bcaudan/jasmine-spec-reporter), License: Apache-2.0
|
||||||
- [karma-chrome-launcher@3.1.0](https://github.com/karma-runner/karma-chrome-launcher), License: MIT
|
- [karma-chrome-launcher@3.1.0](https://github.com/karma-runner/karma-chrome-launcher), License: MIT
|
||||||
- [karma-coverage-istanbul-reporter@2.1.0](https://github.com/mattlewis92/karma-coverage-istanbul-reporter), License: MIT
|
- [karma-coverage-istanbul-reporter@2.1.1](https://github.com/mattlewis92/karma-coverage-istanbul-reporter), License: MIT
|
||||||
- [karma-jasmine-html-reporter@1.4.2](https://github.com/dfederm/karma-jasmine-html-reporter), License: MIT
|
- [karma-jasmine-html-reporter@1.5.3](https://github.com/dfederm/karma-jasmine-html-reporter), License: MIT
|
||||||
- [karma-jasmine@2.0.1](https://github.com/karma-runner/karma-jasmine), License: MIT
|
- [karma-jasmine@3.1.1](https://github.com/karma-runner/karma-jasmine), License: MIT
|
||||||
- [karma@4.3.0](https://github.com/karma-runner/karma), License: MIT
|
- [karma@4.4.1](https://github.com/karma-runner/karma), License: MIT
|
||||||
- [npm-license-crawler@0.2.1](https://github.com/mwittig/npm-license-crawler), License: BSD-3-Clause
|
- [npm-license-crawler@0.2.1](https://github.com/mwittig/npm-license-crawler), License: BSD-3-Clause
|
||||||
- [npm-run-all@4.1.5](https://github.com/mysticatea/npm-run-all), License: MIT
|
- [prettier@2.0.2](https://github.com/prettier/prettier), License: MIT
|
||||||
- [prettier@1.18.2](https://github.com/prettier/prettier), License: MIT
|
- [protractor@5.4.3](https://github.com/angular/protractor), License: MIT
|
||||||
- [protractor@5.4.2](https://github.com/angular/protractor), License: MIT
|
- [resize-observer-polyfill@1.5.1](https://github.com/que-etc/resize-observer-polyfill), License: MIT
|
||||||
- [resize-observer-polyfill@1.5.1](https://github.com/que-etc/resize-observer-polyfill), License: MIT
|
- [ts-node@8.8.1](https://github.com/TypeStrong/ts-node), License: MIT
|
||||||
- [source-map-explorer@2.0.1](https://github.com/danvk/source-map-explorer), License: Apache-2.0
|
- [tslint@6.1.0](https://github.com/palantir/tslint), License: Apache-2.0
|
||||||
- [ts-node@8.3.0](https://github.com/TypeStrong/ts-node), License: MIT
|
- [tsutils@3.17.1](https://github.com/ajafff/tsutils), License: MIT
|
||||||
- [tslint@5.19.0](https://github.com/palantir/tslint), License: Apache-2.0
|
- [typescript@3.8.3](https://github.com/Microsoft/TypeScript), License: Apache-2.0
|
||||||
- [tsutils@3.17.1](https://github.com/ajafff/tsutils), License: MIT
|
|
||||||
- [typescript@3.5.3](https://github.com/Microsoft/TypeScript), License: Apache-2.0
|
|
||||||
- [webpack-bundle-analyzer@3.4.1](https://github.com/webpack-contrib/webpack-bundle-analyzer), License: MIT
|
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"projectType": "application",
|
"projectType": "application",
|
||||||
"schematics": {
|
"schematics": {
|
||||||
"@schematics/angular:component": {
|
"@schematics/angular:component": {
|
||||||
"styleext": "scss"
|
"style": "scss"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "",
|
"root": "",
|
||||||
@ -22,7 +22,7 @@
|
|||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"polyfills": "src/polyfills.ts",
|
"polyfills": "src/polyfills.ts",
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
"aot": false,
|
"aot": true,
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/assets",
|
"src/assets",
|
||||||
"src/manifest.json",
|
"src/manifest.json",
|
||||||
@ -43,15 +43,21 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": ["src/styles.scss"],
|
"styles": ["src/styles.scss"],
|
||||||
"scripts": ["node_modules/tinymce/tinymce.min.js"],
|
"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"
|
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"fileReplacements": [{
|
"fileReplacements": [
|
||||||
"replace": "src/environments/environment.ts",
|
{
|
||||||
"with": "src/environments/environment.prod.ts"
|
"replace": "src/environments/environment.ts",
|
||||||
}],
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
"optimization": true,
|
"optimization": true,
|
||||||
"outputHashing": "all",
|
"outputHashing": "all",
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
@ -62,13 +68,25 @@
|
|||||||
"vendorChunk": false,
|
"vendorChunk": false,
|
||||||
"buildOptimizer": true,
|
"buildOptimizer": true,
|
||||||
"serviceWorker": true,
|
"serviceWorker": true,
|
||||||
"budgets": [{
|
"budgets": [
|
||||||
"type": "initial",
|
{
|
||||||
"maximumWarning": "5mb",
|
"type": "initial",
|
||||||
"maximumError": "10mb"
|
"maximumWarning": "5mb",
|
||||||
}]
|
"maximumError": "10mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "6kb"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"es5": {
|
"es5": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "6kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
"tsConfig": "./tsconfig-es5.app.json"
|
"tsConfig": "./tsconfig-es5.app.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "OpenSlides3-Client",
|
"name": "OpenSlides3-Client",
|
||||||
"version": "3.1.1",
|
"version": "3.2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git://github.com/OpenSlides/OpenSlides.git"
|
"url": "git://github.com/OpenSlides/OpenSlides.git"
|
||||||
@ -10,19 +10,20 @@
|
|||||||
"README": "https://github.com/OpenSlides/OpenSlides/blob/master/client/README.md",
|
"README": "https://github.com/OpenSlides/OpenSlides/blob/master/client/README.md",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"ng-high-memory": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng",
|
|
||||||
"start": "ng serve --proxy-config proxy.conf.json --host=0.0.0.0",
|
"start": "ng serve --proxy-config proxy.conf.json --host=0.0.0.0",
|
||||||
"start-es5": "ng serve --proxy-config proxy.conf.json --host=0.0.0.0 --configuration es5",
|
"start-es5": "ng serve --proxy-config proxy.conf.json --host=0.0.0.0 --configuration es5",
|
||||||
"build": "npm run ng-high-memory -- build --prod",
|
"build": "ng build --prod",
|
||||||
"build-debug": "npm run ng-high-memory -- build",
|
"postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points",
|
||||||
|
"build-debug": "ng build",
|
||||||
"test": "ng test",
|
"test": "ng test",
|
||||||
"test-silently": "npm run test -- --watch=false --no-progress --browsers=ChromeHeadlessNoSandbox",
|
"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-check": "ng lint",
|
||||||
"lint-write": "ng lint --fix",
|
"lint-write": "ng lint --fix",
|
||||||
"e2e": "ng e2e",
|
"e2e": "ng e2e",
|
||||||
"licenses": "node src/crawler.js",
|
"licenses": "node src/crawler.js",
|
||||||
"compodoc": "./node_modules/.bin/compodoc --hideGenerator -p tsconfig.app.json -n 'OpenSlides Documentation' -d ../Compodoc -s -o -r",
|
"compodoc": "./node_modules/.bin/compodoc --hideGenerator -p tsconfig.app.json -n 'OpenSlides Documentation' -d ../Compodoc -s -o -r",
|
||||||
"extract": "ngx-translate-extract -i ./src -o ./src/assets/i18n/template-en.pot --clean --sort --format pot -m _",
|
"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": "./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",
|
"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",
|
||||||
"prettify-check": "prettier --config ./.prettierrc --list-different \"src/{app,environments}/**/*{.ts,.js,.json,.css,.scss}\"",
|
"prettify-check": "prettier --config ./.prettierrc --list-different \"src/{app,environments}/**/*{.ts,.js,.json,.css,.scss}\"",
|
||||||
@ -31,79 +32,81 @@
|
|||||||
"cleanup-win": "npm run prettify-write & npm run lint-write"
|
"cleanup-win": "npm run prettify-write & npm run lint-write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "~8.2.4",
|
"@angular/animations": "~9.1.0",
|
||||||
"@angular/cdk": "~8.1.4",
|
"@angular/cdk": "~9.2.0",
|
||||||
"@angular/cdk-experimental": "~8.1.4",
|
"@angular/cdk-experimental": "~9.2.0",
|
||||||
"@angular/common": "~8.2.4",
|
"@angular/common": "~9.1.0",
|
||||||
"@angular/compiler": "~8.2.4",
|
"@angular/compiler": "~9.1.0",
|
||||||
"@angular/core": "~8.2.4",
|
"@angular/core": "~9.1.0",
|
||||||
"@angular/forms": "~8.2.4",
|
"@angular/forms": "~9.1.0",
|
||||||
"@angular/material": "~8.1.4",
|
"@angular/material": "~9.2.0",
|
||||||
"@angular/material-moment-adapter": "~8.1.4",
|
"@angular/material-moment-adapter": "~9.2.0",
|
||||||
"@angular/platform-browser": "~8.2.4",
|
"@angular/platform-browser": "~9.1.0",
|
||||||
"@angular/platform-browser-dynamic": "~8.2.4",
|
"@angular/platform-browser-dynamic": "~9.1.0",
|
||||||
"@angular/pwa": "^0.803.1",
|
"@angular/router": "~9.1.0",
|
||||||
"@angular/router": "~8.2.4",
|
"@angular/service-worker": "~9.1.0",
|
||||||
"@angular/service-worker": "~8.2.4",
|
"@ngx-pwa/local-storage": "~9.0.2",
|
||||||
"@ngx-pwa/local-storage": "~8.2.1",
|
"@ngx-translate/core": "~12.1.2",
|
||||||
"@ngx-translate/core": "~11.0.1",
|
|
||||||
"@ngx-translate/http-loader": "^4.0.0",
|
"@ngx-translate/http-loader": "^4.0.0",
|
||||||
"@pebula/ngrid": "1.0.0-rc.9",
|
"@pebula/ngrid": "2.0.0-rc.1",
|
||||||
"@pebula/ngrid-material": "1.0.0-rc.9",
|
"@pebula/ngrid-material": "2.0.0-rc.1",
|
||||||
"@pebula/utils": "1.0.0",
|
"@pebula/utils": "1.0.2",
|
||||||
"@tinymce/tinymce-angular": "^3.2.0",
|
"@tinymce/tinymce-angular": "^3.6.0",
|
||||||
"acorn": "^7.0.0",
|
"@videojs/http-streaming": "^1.13.3",
|
||||||
"core-js": "^3.2.1",
|
"acorn": "^7.1.0",
|
||||||
"css-element-queries": "^1.2.1",
|
"chart.js": "^2.9.2",
|
||||||
|
"core-js": "^3.6.4",
|
||||||
|
"css-element-queries": "^1.2.3",
|
||||||
"exceljs": "1.15.0",
|
"exceljs": "1.15.0",
|
||||||
"file-saver": "^2.0.2",
|
"file-saver": "^2.0.2",
|
||||||
"hammerjs": "^2.0.8",
|
|
||||||
"lz4js": "^0.2.0",
|
"lz4js": "^0.2.0",
|
||||||
"material-icon-font": "git+https://github.com/petergng/materialIconFont.git",
|
"material-icon-font": "git+https://github.com/petergng/materialIconFont.git",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"ng2-pdf-viewer": "^5.3.4",
|
"ng2-charts": "^2.3.0",
|
||||||
"ngx-file-drop": "~8.0.7",
|
"ng2-pdf-viewer": "^6.1.2",
|
||||||
"ngx-mat-select-search": "^1.8.0",
|
"ngx-device-detector": "^1.4.4",
|
||||||
"ngx-material-timepicker": "^4.0.2",
|
"ngx-file-drop": "^9.0.1",
|
||||||
|
"ngx-mat-select-search": "^2.1.2",
|
||||||
|
"ngx-material-timepicker": "^5.5.1",
|
||||||
"ngx-papaparse": "^4.0.2",
|
"ngx-papaparse": "^4.0.2",
|
||||||
"pdfmake": "^0.1.58",
|
"pdfmake": "^0.1.63",
|
||||||
"po2json": "^1.0.0-alpha",
|
"po2json": "^1.0.0-beta-2",
|
||||||
"rxjs": "^6.5.2",
|
"rxjs": "^6.5.4",
|
||||||
"tinymce": "^5.0.14",
|
"tinymce": "5.2.2",
|
||||||
"tslib": "^1.10.0",
|
"tslib": "^1.10.0",
|
||||||
"uuid": "^3.3.2",
|
"video.js": "^7.7.6",
|
||||||
"zone.js": "~0.9.1"
|
"zone.js": "~0.10.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "~0.803.2",
|
"@angular-devkit/build-angular": "~0.901.9",
|
||||||
"@angular/cli": "~8.3.2",
|
"@angular-devkit/schematics": "^9.0.6",
|
||||||
"@angular/compiler-cli": "~8.2.4",
|
"@angular/cli": "~9.1.0",
|
||||||
"@angular/language-service": "~8.2.4",
|
"@angular/compiler-cli": "~9.1.0",
|
||||||
"@biesbjerg/ngx-translate-extract": "^3.0.5",
|
"@angular/language-service": "~9.1.0",
|
||||||
|
"@biesbjerg/ngx-translate-extract": "^6.0.3",
|
||||||
|
"@biesbjerg/ngx-translate-extract-marker": "^1.0.0",
|
||||||
"@compodoc/compodoc": "^1.1.8",
|
"@compodoc/compodoc": "^1.1.8",
|
||||||
|
"@schematics/angular": "^9.0.6",
|
||||||
"@types/jasmine": "^3.3.9",
|
"@types/jasmine": "^3.3.9",
|
||||||
"@types/jasminewd2": "^2.0.6",
|
"@types/jasminewd2": "^2.0.6",
|
||||||
"@types/node": "~12.7.2",
|
"@types/node": "^13.9.8",
|
||||||
"@types/yargs": "^13.0.0",
|
"@types/yargs": "^15.0.4",
|
||||||
"codelyzer": "^5.0.1",
|
"codelyzer": "^5.1.2",
|
||||||
"husky": "^3.0.4",
|
"husky": "^4.2.3",
|
||||||
"jasmine-core": "~3.4.0",
|
"jasmine-core": "~3.5.0",
|
||||||
"jasmine-spec-reporter": "~4.2.1",
|
"jasmine-spec-reporter": "~5.0.1",
|
||||||
"karma": "^4.1.0",
|
"karma": "^4.4.1",
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
"karma-coverage-istanbul-reporter": "^2.0.5",
|
"karma-coverage-istanbul-reporter": "^2.0.5",
|
||||||
"karma-jasmine": "~2.0.1",
|
"karma-jasmine": "~3.1.1",
|
||||||
"karma-jasmine-html-reporter": "^1.4.0",
|
"karma-jasmine-html-reporter": "^1.4.0",
|
||||||
"npm-license-crawler": "^0.2.1",
|
"npm-license-crawler": "^0.2.1",
|
||||||
"npm-run-all": "^4.1.5",
|
"prettier": "^2.0.5",
|
||||||
"prettier": "^1.19.1",
|
"protractor": "^5.4.3",
|
||||||
"protractor": "^5.4.2",
|
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"source-map-explorer": "^2.0.1",
|
"ts-node": "~8.8.1",
|
||||||
"ts-node": "~8.3.0",
|
"tslint": "~6.1.0",
|
||||||
"tslint": "~5.19.0",
|
|
||||||
"tsutils": "3.17.1",
|
"tsutils": "3.17.1",
|
||||||
"typescript": "~3.5.3",
|
"typescript": "~3.8.3"
|
||||||
"webpack-bundle-analyzer": "^3.3.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import { LoginPrivacyPolicyComponent } from './site/login/components/login-priva
|
|||||||
import { LoginWrapperComponent } from './site/login/components/login-wrapper/login-wrapper.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 { ResetPasswordConfirmComponent } from './site/login/components/reset-password-confirm/reset-password-confirm.component';
|
||||||
import { ResetPasswordComponent } from './site/login/components/reset-password/reset-password.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
|
* Global app routing
|
||||||
@ -20,7 +21,8 @@ const routes: Routes = [
|
|||||||
{ path: 'reset-password', component: ResetPasswordComponent },
|
{ path: 'reset-password', component: ResetPasswordComponent },
|
||||||
{ path: 'reset-password-confirm', component: ResetPasswordConfirmComponent },
|
{ path: 'reset-password-confirm', component: ResetPasswordConfirmComponent },
|
||||||
{ path: 'legalnotice', component: LoginLegalNoticeComponent },
|
{ path: 'legalnotice', component: LoginLegalNoticeComponent },
|
||||||
{ path: 'privacypolicy', component: LoginPrivacyPolicyComponent }
|
{ path: 'privacypolicy', component: LoginPrivacyPolicyComponent },
|
||||||
|
{ path: 'unsupported-browser', component: UnsupportedBrowserComponent }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
.content {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,8 @@ describe('AppComponent', () => {
|
|||||||
imports: [E2EImportsModule]
|
imports: [E2EImportsModule]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
servertimeService = TestBed.get(ServertimeService);
|
servertimeService = TestBed.inject(ServertimeService);
|
||||||
translate = TestBed.get(TranslateService);
|
translate = TestBed.inject(TranslateService);
|
||||||
spyOn(servertimeService, 'startScheduler').and.stub();
|
spyOn(servertimeService, 'startScheduler').and.stub();
|
||||||
spyOn(translate, 'addLangs').and.stub();
|
spyOn(translate, 'addLangs').and.stub();
|
||||||
spyOn(translate, 'setDefaultLang').and.stub();
|
spyOn(translate, 'setDefaultLang').and.stub();
|
||||||
|
@ -17,6 +17,7 @@ import { PrioritizeService } from './core/core-services/prioritize.service';
|
|||||||
import { RoutingStateService } from './core/ui-services/routing-state.service';
|
import { RoutingStateService } from './core/ui-services/routing-state.service';
|
||||||
import { ServertimeService } from './core/core-services/servertime.service';
|
import { ServertimeService } from './core/core-services/servertime.service';
|
||||||
import { ThemeService } from './core/ui-services/theme.service';
|
import { ThemeService } from './core/ui-services/theme.service';
|
||||||
|
import { VotingBannerService } from './core/ui-services/voting-banner.service';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
/**
|
/**
|
||||||
@ -25,6 +26,12 @@ declare global {
|
|||||||
*/
|
*/
|
||||||
interface Array<T> {
|
interface Array<T> {
|
||||||
flatMap(o: any): any[];
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -79,7 +86,8 @@ export class AppComponent {
|
|||||||
dataStoreUpgradeService: DataStoreUpgradeService, // to start it.
|
dataStoreUpgradeService: DataStoreUpgradeService, // to start it.
|
||||||
prioritizeService: PrioritizeService,
|
prioritizeService: PrioritizeService,
|
||||||
pingService: PingService,
|
pingService: PingService,
|
||||||
routingState: RoutingStateService
|
routingState: RoutingStateService,
|
||||||
|
votingBannerService: VotingBannerService // needed for initialisation
|
||||||
) {
|
) {
|
||||||
// manually add the supported languages
|
// manually add the supported languages
|
||||||
translate.addLangs(['en', 'de', 'cs', 'ru']);
|
translate.addLangs(['en', 'de', 'cs', 'ru']);
|
||||||
@ -91,8 +99,8 @@ export class AppComponent {
|
|||||||
translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en');
|
translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en');
|
||||||
|
|
||||||
// change default JS functions
|
// change default JS functions
|
||||||
this.overloadArrayToString();
|
this.overloadArrayFunctions();
|
||||||
this.overloadFlatMap();
|
this.overloadSetFunctions();
|
||||||
this.overloadModulo();
|
this.overloadModulo();
|
||||||
|
|
||||||
// Wait until the App reaches a stable state.
|
// Wait until the App reaches a stable state.
|
||||||
@ -106,45 +114,84 @@ export class AppComponent {
|
|||||||
.subscribe(() => servertimeService.startScheduler());
|
.subscribe(() => servertimeService.startScheduler());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private overloadArrayFunctions(): void {
|
||||||
* Function to alter the normal Array.toString - function
|
Object.defineProperty(Array.prototype, 'toString', {
|
||||||
*
|
value: function (): string {
|
||||||
* Will add a whitespace after a comma and shorten the output to
|
let string = '';
|
||||||
* three strings.
|
const iterations = Math.min(this.length, 3);
|
||||||
*
|
|
||||||
* TODO: There might be a better place for overloading functions than app.component
|
|
||||||
* TODO: Overloading can be extended to more functions.
|
|
||||||
*/
|
|
||||||
private overloadArrayToString(): void {
|
|
||||||
Array.prototype.toString = function(): string {
|
|
||||||
let string = '';
|
|
||||||
const iterations = Math.min(this.length, 3);
|
|
||||||
|
|
||||||
for (let i = 0; i <= iterations; i++) {
|
for (let i = 0; i <= iterations; i++) {
|
||||||
if (i < iterations) {
|
if (i < iterations) {
|
||||||
string += this[i];
|
string += this[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i < iterations - 1) {
|
if (i < iterations - 1) {
|
||||||
string += ', ';
|
string += ', ';
|
||||||
} else if (i === iterations && this.length > iterations) {
|
} else if (i === iterations && this.length > iterations) {
|
||||||
string += ', ...';
|
string += ', ...';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return 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 an implementation of flatMap.
|
* Adds some functions to Set.
|
||||||
* TODO: Remove once flatMap made its way into official JS/TS (ES 2019?)
|
|
||||||
*/
|
*/
|
||||||
private overloadFlatMap(): void {
|
private overloadSetFunctions(): void {
|
||||||
const concat = (x: any, y: any) => x.concat(y);
|
Object.defineProperty(Set.prototype, 'equals', {
|
||||||
const flatMap = (f: any, xs: any) => xs.map(f).reduce(concat, []);
|
value: function <T>(other: Set<T>): boolean {
|
||||||
Array.prototype.flatMap = function(f: any): any[] {
|
const difference = new Set(this);
|
||||||
return flatMap(f, this);
|
for (const elem of other) {
|
||||||
};
|
if (difference.has(elem)) {
|
||||||
|
difference.delete(elem);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !difference.size;
|
||||||
|
},
|
||||||
|
enumerable: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -152,8 +199,11 @@ export class AppComponent {
|
|||||||
* TODO: Remove this, if the remainder operation is changed to modulo.
|
* TODO: Remove this, if the remainder operation is changed to modulo.
|
||||||
*/
|
*/
|
||||||
private overloadModulo(): void {
|
private overloadModulo(): void {
|
||||||
Number.prototype.modulo = function(n: number): number {
|
Object.defineProperty(Number.prototype, 'modulo', {
|
||||||
return ((this % n) + n) % n;
|
value: function (n: number): number {
|
||||||
};
|
return ((this % n) + n) % n;
|
||||||
|
},
|
||||||
|
enumerable: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ import { BrowserModule } from '@angular/platform-browser';
|
|||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||||
|
|
||||||
|
import { StorageModule } from '@ngx-pwa/local-storage';
|
||||||
|
|
||||||
import { AppLoadService } from './core/core-services/app-load.service';
|
import { AppLoadService } from './core/core-services/app-load.service';
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
@ -39,7 +41,8 @@ export function AppLoaderFactory(appLoadService: AppLoadService): () => Promise<
|
|||||||
CoreModule,
|
CoreModule,
|
||||||
LoginModule,
|
LoginModule,
|
||||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
||||||
SlidesModule.forRoot()
|
SlidesModule.forRoot(),
|
||||||
|
StorageModule.forRoot({ IDBNoWrap: false })
|
||||||
],
|
],
|
||||||
providers: [{ provide: APP_INITIALIZER, useFactory: AppLoaderFactory, deps: [AppLoadService], multi: true }],
|
providers: [{ provide: APP_INITIALIZER, useFactory: AppLoaderFactory, deps: [AppLoadService], multi: true }],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
|
@ -2,6 +2,8 @@ import { Title } from '@angular/platform-browser';
|
|||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { Permission } from './core/core-services/operator.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides functionalities that will be used by most components
|
* Provides functionalities that will be used by most components
|
||||||
* currently able to set the title with the suffix ' - OpenSlides'
|
* currently able to set the title with the suffix ' - OpenSlides'
|
||||||
@ -10,6 +12,11 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
* Components in the 'Side'- or 'projector' Folder are BaseComponents
|
* Components in the 'Side'- or 'projector' Folder are BaseComponents
|
||||||
*/
|
*/
|
||||||
export abstract class BaseComponent {
|
export abstract class BaseComponent {
|
||||||
|
/**
|
||||||
|
* To check permissions in templates using permission.[...]
|
||||||
|
*/
|
||||||
|
public permission = Permission;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To manipulate the browser title bar, adds the Suffix "OpenSlides"
|
* To manipulate the browser title bar, adds the Suffix "OpenSlides"
|
||||||
*
|
*
|
||||||
@ -58,7 +65,9 @@ export abstract class BaseComponent {
|
|||||||
mobile: {
|
mobile: {
|
||||||
theme: 'mobile',
|
theme: 'mobile',
|
||||||
plugins: ['autosave', 'lists', 'autolink']
|
plugins: ['autosave', 'lists', 'autolink']
|
||||||
}
|
},
|
||||||
|
relative_urls: false,
|
||||||
|
remove_script_host: true
|
||||||
};
|
};
|
||||||
|
|
||||||
public constructor(protected titleService: Title, protected translate: TranslateService) {
|
public constructor(protected titleService: Title, protected translate: TranslateService) {
|
||||||
|
@ -68,15 +68,10 @@ export class AppLoadService {
|
|||||||
let repository: BaseRepository<any, any, any> = null;
|
let repository: BaseRepository<any, any, any> = null;
|
||||||
repository = this.injector.get(entry.repository);
|
repository = this.injector.get(entry.repository);
|
||||||
repositories.push(repository);
|
repositories.push(repository);
|
||||||
this.modelMapper.registerCollectionElement(
|
this.modelMapper.registerCollectionElement(entry.model, entry.viewModel, repository);
|
||||||
entry.collectionString,
|
|
||||||
entry.model,
|
|
||||||
entry.viewModel,
|
|
||||||
repository
|
|
||||||
);
|
|
||||||
if (this.isSearchableModelEntry(entry)) {
|
if (this.isSearchableModelEntry(entry)) {
|
||||||
this.searchService.registerModel(
|
this.searchService.registerModel(
|
||||||
entry.collectionString,
|
entry.model.COLLECTIONSTRING,
|
||||||
repository,
|
repository,
|
||||||
entry.searchOrder,
|
entry.searchOrder,
|
||||||
entry.openInNewTab
|
entry.openInNewTab
|
||||||
@ -104,11 +99,11 @@ export class AppLoadService {
|
|||||||
private isSearchableModelEntry(entry: ModelEntry | SearchableModelEntry): entry is SearchableModelEntry {
|
private isSearchableModelEntry(entry: ModelEntry | SearchableModelEntry): entry is SearchableModelEntry {
|
||||||
if ((<SearchableModelEntry>entry).searchOrder !== undefined) {
|
if ((<SearchableModelEntry>entry).searchOrder !== undefined) {
|
||||||
// We need to double check, because Typescipt cannot check contructors. If typescript could differentiate
|
// 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
|
// between (ModelConstructor<BaseModel>) and (new (...args: any[]) => (BaseModel & Searchable)),
|
||||||
// to check if the result of the contructor (the model instance) is really a 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())) {
|
if (!isSearchable(new entry.viewModel())) {
|
||||||
throw Error(
|
throw Error(
|
||||||
`Wrong configuration for ${entry.collectionString}: you gave a searchOrder, but the model is not searchable.`
|
`Wrong configuration for ${entry.model.COLLECTIONSTRING}: you gave a searchOrder, but the model is not searchable.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -3,7 +3,7 @@ import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router } from '@
|
|||||||
|
|
||||||
import { FallbackRoutesService } from './fallback-routes.service';
|
import { FallbackRoutesService } from './fallback-routes.service';
|
||||||
import { OpenSlidesService } from './openslides.service';
|
import { OpenSlidesService } from './openslides.service';
|
||||||
import { OperatorService } from './operator.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.
|
* Classical Auth-Guard. Checks if the user has to correct permissions to enter a page, and forwards to login if not.
|
||||||
@ -36,7 +36,7 @@ export class AuthGuard implements CanActivate, CanActivateChild {
|
|||||||
* @param route the route the user wants to navigate to
|
* @param route the route the user wants to navigate to
|
||||||
*/
|
*/
|
||||||
public canActivate(route: ActivatedRouteSnapshot): boolean {
|
public canActivate(route: ActivatedRouteSnapshot): boolean {
|
||||||
const basePerm: string | string[] = route.data.basePerm;
|
const basePerm: Permission | Permission[] = route.data.basePerm;
|
||||||
|
|
||||||
if (!basePerm) {
|
if (!basePerm) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -8,6 +8,7 @@ import { DEFAULT_AUTH_TYPE, UserAuthType } from 'app/shared/models/users/user';
|
|||||||
import { DataStoreService } from './data-store.service';
|
import { DataStoreService } from './data-store.service';
|
||||||
import { HttpService } from './http.service';
|
import { HttpService } from './http.service';
|
||||||
import { OpenSlidesService } from './openslides.service';
|
import { OpenSlidesService } from './openslides.service';
|
||||||
|
import { StorageService } from './storage.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticates an OpenSlides user with username and password
|
* Authenticates an OpenSlides user with username and password
|
||||||
@ -29,7 +30,8 @@ export class AuthService {
|
|||||||
private operator: OperatorService,
|
private operator: OperatorService,
|
||||||
private OpenSlides: OpenSlidesService,
|
private OpenSlides: OpenSlidesService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private DS: DataStoreService
|
private DS: DataStoreService,
|
||||||
|
private storageService: StorageService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,6 +58,9 @@ export class AuthService {
|
|||||||
await this.OpenSlides.afterLoginBootup(response.user_id);
|
await this.OpenSlides.afterLoginBootup(response.user_id);
|
||||||
await this.redirectUser(response.user_id);
|
await this.redirectUser(response.user_id);
|
||||||
} else if (authType === 'saml') {
|
} 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
|
window.location.href = environment.urlPrefix + '/saml/?sso'; // Bye
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported auth type "${authType}"`);
|
throw new Error(`Unsupported auth type "${authType}"`);
|
||||||
@ -67,7 +72,7 @@ export class AuthService {
|
|||||||
* if it wasn't done before.
|
* if it wasn't done before.
|
||||||
*/
|
*/
|
||||||
public async redirectUser(userId: number): Promise<void> {
|
public async redirectUser(userId: number): Promise<void> {
|
||||||
if (!this.OpenSlides.booted) {
|
if (!this.OpenSlides.isBooted) {
|
||||||
await this.OpenSlides.afterLoginBootup(userId);
|
await this.OpenSlides.afterLoginBootup(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,10 +108,12 @@ export class AuthService {
|
|||||||
// We do nothing on failures. Reboot OpenSlides anyway.
|
// We do nothing on failures. Reboot OpenSlides anyway.
|
||||||
}
|
}
|
||||||
this.router.navigate(['/']);
|
this.router.navigate(['/']);
|
||||||
|
await this.storageService.clear();
|
||||||
await this.DS.clear();
|
await this.DS.clear();
|
||||||
await this.operator.setWhoAmI(response);
|
await this.operator.setWhoAmI(response);
|
||||||
await this.OpenSlides.reboot();
|
await this.OpenSlides.reboot();
|
||||||
} else if (authType === 'saml') {
|
} else if (authType === 'saml') {
|
||||||
|
await this.storageService.clear();
|
||||||
await this.DS.clear();
|
await this.DS.clear();
|
||||||
await this.operator.setWhoAmI(null);
|
await this.operator.setWhoAmI(null);
|
||||||
window.location.href = environment.urlPrefix + '/saml/?slo'; // Bye
|
window.location.href = environment.urlPrefix + '/saml/?slo'; // Bye
|
||||||
|
@ -3,9 +3,10 @@ import { Injectable } from '@angular/core';
|
|||||||
import { BaseModel } from '../../shared/models/base/base-model';
|
import { BaseModel } from '../../shared/models/base/base-model';
|
||||||
import { CollectionStringMapperService } from './collection-string-mapper.service';
|
import { CollectionStringMapperService } from './collection-string-mapper.service';
|
||||||
import { DataStoreService, DataStoreUpdateManagerService } from './data-store.service';
|
import { DataStoreService, DataStoreUpdateManagerService } from './data-store.service';
|
||||||
import { WEBSOCKET_ERROR_CODES, WebsocketService } from './websocket.service';
|
import { Mutex } from '../promises/mutex';
|
||||||
|
import { WebsocketService, WEBSOCKET_ERROR_CODES } from './websocket.service';
|
||||||
|
|
||||||
interface AutoupdateFormat {
|
export interface AutoupdateFormat {
|
||||||
/**
|
/**
|
||||||
* All changed (and created) items as their full/restricted data grouped by their collection.
|
* All changed (and created) items as their full/restricted data grouped by their collection.
|
||||||
*/
|
*/
|
||||||
@ -36,6 +37,19 @@ interface AutoupdateFormat {
|
|||||||
all_data: boolean;
|
all_data: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAutoupdateFormat(obj: any): obj is AutoupdateFormat {
|
||||||
|
const format = obj as AutoupdateFormat;
|
||||||
|
return (
|
||||||
|
obj &&
|
||||||
|
typeof obj === 'object' &&
|
||||||
|
format.changed !== undefined &&
|
||||||
|
format.deleted !== undefined &&
|
||||||
|
format.from_change_id !== undefined &&
|
||||||
|
format.to_change_id !== undefined &&
|
||||||
|
format.all_data !== undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the initial update and automatic updates using the {@link WebsocketService}
|
* Handles the initial update and automatic updates using the {@link WebsocketService}
|
||||||
* Incoming objects, usually BaseModels, will be saved in the dataStore (`this.DS`)
|
* Incoming objects, usually BaseModels, will be saved in the dataStore (`this.DS`)
|
||||||
@ -45,6 +59,8 @@ interface AutoupdateFormat {
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class AutoupdateService {
|
export class AutoupdateService {
|
||||||
|
private mutex = new Mutex();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor to create the AutoupdateService. Calls the constructor of the parent class.
|
* Constructor to create the AutoupdateService. Calls the constructor of the parent class.
|
||||||
* @param websocketService
|
* @param websocketService
|
||||||
@ -79,15 +95,17 @@ export class AutoupdateService {
|
|||||||
* Handles the change ids of all autoupdates.
|
* Handles the change ids of all autoupdates.
|
||||||
*/
|
*/
|
||||||
private async storeResponse(autoupdate: AutoupdateFormat): Promise<void> {
|
private async storeResponse(autoupdate: AutoupdateFormat): Promise<void> {
|
||||||
|
const unlock = await this.mutex.lock();
|
||||||
if (autoupdate.all_data) {
|
if (autoupdate.all_data) {
|
||||||
await this.storeAllData(autoupdate);
|
await this.storeAllData(autoupdate);
|
||||||
} else {
|
} else {
|
||||||
await this.storePartialAutoupdate(autoupdate);
|
await this.storePartialAutoupdate(autoupdate);
|
||||||
}
|
}
|
||||||
|
unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores all data from the autoupdate. This means, that the DS is resettet and filled with just the
|
* Stores all data from the autoupdate. This means, that the DS is resetted and filled with just the
|
||||||
* given data from the autoupdate.
|
* given data from the autoupdate.
|
||||||
* @param autoupdate The autoupdate
|
* @param autoupdate The autoupdate
|
||||||
*/
|
*/
|
||||||
@ -116,27 +134,41 @@ export class AutoupdateService {
|
|||||||
|
|
||||||
// Normal autoupdate
|
// Normal autoupdate
|
||||||
if (autoupdate.from_change_id <= maxChangeId + 1 && autoupdate.to_change_id > maxChangeId) {
|
if (autoupdate.from_change_id <= maxChangeId + 1 && autoupdate.to_change_id > maxChangeId) {
|
||||||
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS);
|
await this.injectAutupdateIntoDS(autoupdate, true);
|
||||||
|
|
||||||
// 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]));
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.DS.flushToStorage(autoupdate.to_change_id);
|
|
||||||
|
|
||||||
this.DSUpdateManager.commit(updateSlot, autoupdate.to_change_id);
|
|
||||||
} else {
|
} else {
|
||||||
// autoupdate fully in the future. we are missing something!
|
// 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.requestChanges();
|
this.requestChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async injectAutoupdateIgnoreChangeId(autoupdate: AutoupdateFormat): Promise<void> {
|
||||||
|
const unlock = await this.mutex.lock();
|
||||||
|
console.debug('inject autoupdate', autoupdate);
|
||||||
|
await this.injectAutupdateIntoDS(autoupdate, false);
|
||||||
|
unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
* Creates baseModels for each plain object. If the collection is not registered,
|
||||||
* A console error will be issued and an empty list returned.
|
* A console error will be issued and an empty list returned.
|
||||||
@ -160,9 +192,8 @@ export class AutoupdateService {
|
|||||||
* The server should return an autoupdate with all new data.
|
* The server should return an autoupdate with all new data.
|
||||||
*/
|
*/
|
||||||
public requestChanges(): void {
|
public requestChanges(): void {
|
||||||
const changeId = this.DS.maxChangeId === 0 ? 0 : this.DS.maxChangeId + 1;
|
console.log(`requesting changed objects with DS max change id ${this.DS.maxChangeId}`);
|
||||||
console.log(`requesting changed objects with DS max change id ${changeId}`);
|
this.websocketService.send('getElements', { change_id: this.DS.maxChangeId });
|
||||||
this.websocketService.send('getElements', { change_id: changeId });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,12 +47,11 @@ export class CollectionStringMapperService {
|
|||||||
* @param model
|
* @param model
|
||||||
*/
|
*/
|
||||||
public registerCollectionElement<V extends BaseViewModel<M>, M extends BaseModel>(
|
public registerCollectionElement<V extends BaseViewModel<M>, M extends BaseModel>(
|
||||||
collectionString: string,
|
|
||||||
model: ModelConstructor<M>,
|
model: ModelConstructor<M>,
|
||||||
viewModel: ViewModelConstructor<V>,
|
viewModel: ViewModelConstructor<V>,
|
||||||
repository: BaseRepository<V, M, TitleInformation>
|
repository: BaseRepository<V, M, TitleInformation>
|
||||||
): void {
|
): void {
|
||||||
this.collectionStringMapping[collectionString] = [model, viewModel, repository];
|
this.collectionStringMapping[model.COLLECTIONSTRING] = [model, viewModel, repository];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -160,7 +160,8 @@ interface JsonStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Avoid circular dependencies between `DataStoreUpdateManagerService` and `DataStoreService` and split them into two files
|
* TODO: Avoid circular dependencies between `DataStoreUpdateManagerService` and
|
||||||
|
* `DataStoreService` and split them into two files
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -247,8 +248,17 @@ export class DataStoreUpdateManagerService {
|
|||||||
|
|
||||||
slot.DS.triggerModifiedObservable();
|
slot.DS.triggerModifiedObservable();
|
||||||
|
|
||||||
// serve next slot request
|
this.serveNextSlot();
|
||||||
|
}
|
||||||
|
|
||||||
|
public dropUpdateSlot(): void {
|
||||||
|
this.currentUpdateSlot = null;
|
||||||
|
this.serveNextSlot();
|
||||||
|
}
|
||||||
|
|
||||||
|
private serveNextSlot(): void {
|
||||||
if (this.updateSlotRequests.length > 0) {
|
if (this.updateSlotRequests.length > 0) {
|
||||||
|
console.log('Concurrent update slots');
|
||||||
const request = this.updateSlotRequests.pop();
|
const request = this.updateSlotRequests.pop();
|
||||||
request.resolve();
|
request.resolve();
|
||||||
}
|
}
|
||||||
@ -347,14 +357,21 @@ export class DataStoreService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the DataStore from cache and instantiate all models out of the serialized version.
|
* 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.
|
* @returns The max change id.
|
||||||
*/
|
*/
|
||||||
public async initFromStorage(): Promise<number> {
|
public async initFromStorage(): Promise<number> {
|
||||||
// This promise will be resolved with cached datastore.
|
// This promise will be resolved with cached datastore.
|
||||||
const store = await this.storageService.get<JsonStorage>(DataStoreService.cachePrefix + 'DS');
|
const store = await this.storageService.get<JsonStorage>(DataStoreService.cachePrefix + 'DS');
|
||||||
if (store) {
|
if (!store) {
|
||||||
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this);
|
await this.clear();
|
||||||
|
return this.maxChangeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this);
|
||||||
|
|
||||||
|
try {
|
||||||
// There is a store. Deserialize it
|
// There is a store. Deserialize it
|
||||||
this.jsonStore = store;
|
this.jsonStore = store;
|
||||||
this.modelStore = this.deserializeJsonStore(this.jsonStore);
|
this.modelStore = this.deserializeJsonStore(this.jsonStore);
|
||||||
@ -374,7 +391,8 @@ export class DataStoreService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.DSUpdateManager.commit(updateSlot, maxChangeId, true);
|
this.DSUpdateManager.commit(updateSlot, maxChangeId, true);
|
||||||
} else {
|
} catch (e) {
|
||||||
|
this.DSUpdateManager.dropUpdateSlot();
|
||||||
await this.clear();
|
await this.clear();
|
||||||
}
|
}
|
||||||
return this.maxChangeId;
|
return this.maxChangeId;
|
||||||
@ -648,4 +666,11 @@ export class DataStoreService {
|
|||||||
await this.storageService.set(DataStoreService.cachePrefix + 'DS', this.jsonStore);
|
await this.storageService.set(DataStoreService.cachePrefix + 'DS', this.jsonStore);
|
||||||
await this.storageService.set(DataStoreService.cachePrefix + 'maxChangeId', changeId);
|
await this.storageService.set(DataStoreService.cachePrefix + 'maxChangeId', changeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public print(): void {
|
||||||
|
console.log('Max change id', this.maxChangeId);
|
||||||
|
console.log('json storage');
|
||||||
|
console.log(JSON.stringify(this.jsonStore));
|
||||||
|
console.log(this.modelStore);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { OperatorService } from './operator.service';
|
import { OperatorService, Permission } from './operator.service';
|
||||||
|
|
||||||
export interface AuthGuardFallbackEntry {
|
export interface AuthGuardFallbackEntry {
|
||||||
route: string;
|
route: string;
|
||||||
weight: number;
|
weight: number;
|
||||||
permission: string;
|
permission: Permission;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -10,7 +10,7 @@ describe('HttpService', () => {
|
|||||||
});
|
});
|
||||||
// TODO: Write a working Test
|
// TODO: Write a working Test
|
||||||
// it('should be created', () => {
|
// it('should be created', () => {
|
||||||
// const service: HttpService = TestBed.get(HttpService);
|
// const service: HttpService = TestBed.inject(HttpService);
|
||||||
// expect(service).toBeTruthy();
|
// expect(service).toBeTruthy();
|
||||||
// });
|
// });
|
||||||
});
|
});
|
||||||
|
@ -3,6 +3,7 @@ import { Injectable } from '@angular/core';
|
|||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { AutoupdateFormat, AutoupdateService, isAutoupdateFormat } from './autoupdate.service';
|
||||||
import { OpenSlidesStatusService } from './openslides-status.service';
|
import { OpenSlidesStatusService } from './openslides-status.service';
|
||||||
import { formatQueryParams, QueryParams } from '../definitions/query-params';
|
import { formatQueryParams, QueryParams } from '../definitions/query-params';
|
||||||
|
|
||||||
@ -17,12 +18,12 @@ export enum HTTPMethod {
|
|||||||
DELETE = 'delete'
|
DELETE = 'delete'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DetailResponse {
|
export interface ErrorDetailResponse {
|
||||||
detail: string | string[];
|
detail: string | string[];
|
||||||
args?: string[];
|
args?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDetailResponse(obj: any): obj is DetailResponse {
|
function isErrorDetailResponse(obj: any): obj is ErrorDetailResponse {
|
||||||
return (
|
return (
|
||||||
obj &&
|
obj &&
|
||||||
typeof obj === 'object' &&
|
typeof obj === 'object' &&
|
||||||
@ -31,6 +32,15 @@ function isDetailResponse(obj: any): obj is DetailResponse {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AutoupdateResponse {
|
||||||
|
autoupdate: AutoupdateFormat;
|
||||||
|
data?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAutoupdateReponse(obj: any): obj is AutoupdateResponse {
|
||||||
|
return obj && typeof obj === 'object' && isAutoupdateFormat((obj as AutoupdateResponse).autoupdate);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for managing HTTP requests. Allows to send data for every method. Also (TODO) will do generic error handling.
|
* Service for managing HTTP requests. Allows to send data for every method. Also (TODO) will do generic error handling.
|
||||||
*/
|
*/
|
||||||
@ -55,7 +65,8 @@ export class HttpService {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private translate: TranslateService,
|
private translate: TranslateService,
|
||||||
private OSStatus: OpenSlidesStatusService
|
private OSStatus: OpenSlidesStatusService,
|
||||||
|
private autoupdateService: AutoupdateService
|
||||||
) {
|
) {
|
||||||
this.defaultHeaders = new HttpHeaders().set('Content-Type', 'application/json');
|
this.defaultHeaders = new HttpHeaders().set('Content-Type', 'application/json');
|
||||||
}
|
}
|
||||||
@ -82,7 +93,7 @@ export class HttpService {
|
|||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
// end early, if we are in history mode
|
// end early, if we are in history mode
|
||||||
if (this.OSStatus.isInHistoryMode && method !== HTTPMethod.GET) {
|
if (this.OSStatus.isInHistoryMode && method !== HTTPMethod.GET) {
|
||||||
throw this.handleError('You cannot make changes while in history mode');
|
throw this.processError('You cannot make changes while in history mode');
|
||||||
}
|
}
|
||||||
|
|
||||||
// there is a current bug with the responseType.
|
// there is a current bug with the responseType.
|
||||||
@ -108,9 +119,10 @@ export class HttpService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.http.request<T>(method, url, options).toPromise();
|
const responseData: T = await this.http.request<T>(method, url, options).toPromise();
|
||||||
} catch (e) {
|
return this.processResponse(responseData);
|
||||||
throw this.handleError(e);
|
} catch (error) {
|
||||||
|
throw this.processError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +132,7 @@ export class HttpService {
|
|||||||
* @param e The error thrown.
|
* @param e The error thrown.
|
||||||
* @returns The prepared and translated message for the user
|
* @returns The prepared and translated message for the user
|
||||||
*/
|
*/
|
||||||
private handleError(e: any): string {
|
private processError(e: any): string {
|
||||||
let error = this.translate.instant('Error') + ': ';
|
let error = this.translate.instant('Error') + ': ';
|
||||||
// If the error is a string already, return it.
|
// If the error is a string already, return it.
|
||||||
if (typeof e === 'string') {
|
if (typeof e === 'string') {
|
||||||
@ -142,15 +154,16 @@ export class HttpService {
|
|||||||
} else if (!e.error) {
|
} else if (!e.error) {
|
||||||
error += this.translate.instant("The server didn't respond.");
|
error += this.translate.instant("The server didn't respond.");
|
||||||
} else if (typeof e.error === 'object') {
|
} else if (typeof e.error === 'object') {
|
||||||
if (isDetailResponse(e.error)) {
|
if (isErrorDetailResponse(e.error)) {
|
||||||
error += this.processDetailResponse(e.error);
|
error += this.processErrorDetailResponse(e.error);
|
||||||
} else {
|
} else {
|
||||||
error = Object.keys(e.error)
|
const errorList = Object.keys(e.error).map(key => {
|
||||||
.map(key => {
|
const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
|
||||||
const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
|
return `${this.translate.instant(capitalizedKey)}: ${this.processErrorDetailResponse(
|
||||||
return this.translate.instant(capitalizedKey) + ': ' + this.processDetailResponse(e.error[key]);
|
e.error[key]
|
||||||
})
|
)}`;
|
||||||
.join(', ');
|
});
|
||||||
|
error = errorList.join(', ');
|
||||||
}
|
}
|
||||||
} else if (e.status === 500) {
|
} else if (e.status === 500) {
|
||||||
error += this.translate.instant('A server error occured. Please contact your system administrator.');
|
error += this.translate.instant('A server error occured. Please contact your system administrator.');
|
||||||
@ -169,11 +182,9 @@ export class HttpService {
|
|||||||
* @param str a string or a string array to join together.
|
* @param str a string or a string array to join together.
|
||||||
* @returns Error text(s) as single string
|
* @returns Error text(s) as single string
|
||||||
*/
|
*/
|
||||||
private processDetailResponse(response: DetailResponse): string {
|
private processErrorDetailResponse(response: ErrorDetailResponse): string {
|
||||||
let message: string;
|
let message: string;
|
||||||
if (response instanceof Array) {
|
if (response.detail instanceof Array) {
|
||||||
message = response.join(' ');
|
|
||||||
} else if (response.detail instanceof Array) {
|
|
||||||
message = response.detail.join(' ');
|
message = response.detail.join(' ');
|
||||||
} else {
|
} else {
|
||||||
message = response.detail;
|
message = response.detail;
|
||||||
@ -188,6 +199,14 @@ export class HttpService {
|
|||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private processResponse<T>(responseData: T): T {
|
||||||
|
if (isAutoupdateReponse(responseData)) {
|
||||||
|
this.autoupdateService.injectAutoupdateIgnoreChangeId(responseData.autoupdate);
|
||||||
|
responseData = responseData.data;
|
||||||
|
}
|
||||||
|
return responseData;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a get on a path with a certain object
|
* Executes a get on a path with a certain object
|
||||||
* @param path The path to send the request to.
|
* @param path The path to send the request to.
|
||||||
|
@ -2,6 +2,8 @@ import { Injectable } from '@angular/core';
|
|||||||
|
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
import { Permission } from './operator.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This represents one entry in the main menu
|
* This represents one entry in the main menu
|
||||||
*/
|
*/
|
||||||
@ -28,7 +30,7 @@ export interface MainMenuEntry {
|
|||||||
/**
|
/**
|
||||||
* The permission to see the entry.
|
* The permission to see the entry.
|
||||||
*/
|
*/
|
||||||
permission: string;
|
permission: Permission;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { BannerDefinition, BannerService } from '../ui-services/banner.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This service handles everything connected with being offline.
|
* This service handles everything connected with being offline.
|
||||||
*
|
*
|
||||||
@ -16,6 +20,16 @@ export class OfflineService {
|
|||||||
* BehaviorSubject to receive further status values.
|
* BehaviorSubject to receive further status values.
|
||||||
*/
|
*/
|
||||||
private offline = new BehaviorSubject<boolean>(false);
|
private offline = new BehaviorSubject<boolean>(false);
|
||||||
|
private bannerDefinition: BannerDefinition = {
|
||||||
|
text: _('Offline mode'),
|
||||||
|
icon: 'cloud_off'
|
||||||
|
};
|
||||||
|
|
||||||
|
public constructor(private banner: BannerService, translate: TranslateService) {
|
||||||
|
translate.onLangChange.subscribe(() => {
|
||||||
|
this.bannerDefinition.text = translate.instant(this.bannerDefinition.text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines of you are either in Offline mode or not connected via websocket
|
* Determines of you are either in Offline mode or not connected via websocket
|
||||||
@ -33,7 +47,7 @@ export class OfflineService {
|
|||||||
if (!this.offline.getValue()) {
|
if (!this.offline.getValue()) {
|
||||||
console.log('offline because whoami failed.');
|
console.log('offline because whoami failed.');
|
||||||
}
|
}
|
||||||
this.offline.next(true);
|
this.goOffline();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,7 +57,15 @@ export class OfflineService {
|
|||||||
if (!this.offline.getValue()) {
|
if (!this.offline.getValue()) {
|
||||||
console.log('offline because connection lost.');
|
console.log('offline because connection lost.');
|
||||||
}
|
}
|
||||||
|
this.goOffline();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to set offline status
|
||||||
|
*/
|
||||||
|
private goOffline(): void {
|
||||||
this.offline.next(true);
|
this.offline.next(true);
|
||||||
|
this.banner.addBanner(this.bannerDefinition);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,5 +73,6 @@ export class OfflineService {
|
|||||||
*/
|
*/
|
||||||
public goOnline(): void {
|
public goOnline(): void {
|
||||||
this.offline.next(false);
|
this.offline.next(false);
|
||||||
|
this.banner.removeBanner(this.bannerDefinition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { History } from 'app/shared/models/core/history';
|
import { History } from 'app/shared/models/core/history';
|
||||||
|
import { BannerDefinition, BannerService } from '../ui-services/banner.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds information about OpenSlides. This is not included into other services to
|
* Holds information about OpenSlides. This is not included into other services to
|
||||||
@ -14,6 +15,9 @@ export class OpenSlidesStatusService {
|
|||||||
* in History mode, saves the history point.
|
* in History mode, saves the history point.
|
||||||
*/
|
*/
|
||||||
private history: History = null;
|
private history: History = null;
|
||||||
|
private bannerDefinition: BannerDefinition = {
|
||||||
|
type: 'history'
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns, if OpenSlides is in the history mode.
|
* Returns, if OpenSlides is in the history mode.
|
||||||
@ -27,7 +31,7 @@ export class OpenSlidesStatusService {
|
|||||||
/**
|
/**
|
||||||
* Ctor, does nothing.
|
* Ctor, does nothing.
|
||||||
*/
|
*/
|
||||||
public constructor() {}
|
public constructor(private banner: BannerService) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls the getLocaleString function of the history object, if present.
|
* Calls the getLocaleString function of the history object, if present.
|
||||||
@ -44,6 +48,7 @@ export class OpenSlidesStatusService {
|
|||||||
*/
|
*/
|
||||||
public enterHistoryMode(history: History): void {
|
public enterHistoryMode(history: History): void {
|
||||||
this.history = history;
|
this.history = history;
|
||||||
|
this.banner.addBanner(this.bannerDefinition);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,5 +56,6 @@ export class OpenSlidesStatusService {
|
|||||||
*/
|
*/
|
||||||
public leaveHistoryMode(): void {
|
public leaveHistoryMode(): void {
|
||||||
this.history = null;
|
this.history = null;
|
||||||
|
this.banner.removeBanner(this.bannerDefinition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,10 +130,7 @@ export class OpenSlidesService {
|
|||||||
* Init DS from cache and after this start the websocket service.
|
* Init DS from cache and after this start the websocket service.
|
||||||
*/
|
*/
|
||||||
private async setupDataStoreAndWebSocket(): Promise<void> {
|
private async setupDataStoreAndWebSocket(): Promise<void> {
|
||||||
let changeId = await this.DS.initFromStorage();
|
const changeId = await this.DS.initFromStorage();
|
||||||
if (changeId > 0) {
|
|
||||||
changeId += 1;
|
|
||||||
}
|
|
||||||
// disconnect the WS connection, if there was one. This is needed
|
// disconnect the WS connection, if there was one. This is needed
|
||||||
// to update the connection parameters, namely the cookies. If the user
|
// to update the connection parameters, namely the cookies. If the user
|
||||||
// is changed, the WS needs to reconnect, so the new connection holds the new
|
// is changed, the WS needs to reconnect, so the new connection holds the new
|
||||||
@ -141,7 +138,7 @@ export class OpenSlidesService {
|
|||||||
if (this.websocketService.isConnected) {
|
if (this.websocketService.isConnected) {
|
||||||
await this.websocketService.close(); // Wait for the disconnect.
|
await this.websocketService.close(); // Wait for the disconnect.
|
||||||
}
|
}
|
||||||
await this.websocketService.connect({ changeId: changeId }); // Request changes after changeId.
|
await this.websocketService.connect(changeId); // Request changes after changeId.
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,7 +21,40 @@ import { UserRepositoryService } from '../repositories/users/user-repository.ser
|
|||||||
* Permissions on the client are just strings. This makes clear, that
|
* Permissions on the client are just strings. This makes clear, that
|
||||||
* permissions instead of arbitrary strings should be given.
|
* permissions instead of arbitrary strings should be given.
|
||||||
*/
|
*/
|
||||||
export type Permission = string;
|
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',
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Response format of the WhoAmI request.
|
* Response format of the WhoAmI request.
|
||||||
@ -252,6 +285,10 @@ export class OperatorService implements OnAfterAppsLoaded {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async clearWhoAmIFromStorage(): Promise<void> {
|
||||||
|
await this.storageService.remove(WHOAMI_STORAGE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the operator user. Will be saved to storage
|
* Sets the operator user. Will be saved to storage
|
||||||
* @param user The new operator.
|
* @param user The new operator.
|
||||||
@ -390,12 +427,12 @@ export class OperatorService implements OnAfterAppsLoaded {
|
|||||||
} else {
|
} else {
|
||||||
// Anonymous or users in the default group.
|
// Anonymous or users in the default group.
|
||||||
if (!this.user || this.user.groups_id.length === 0) {
|
if (!this.user || this.user.groups_id.length === 0) {
|
||||||
const defaultGroup = this.DS.get<Group>('users/group', 1);
|
const defaultGroup: Group = this.DS.get<Group>('users/group', 1);
|
||||||
if (defaultGroup && defaultGroup.permissions instanceof Array) {
|
if (defaultGroup && defaultGroup.permissions instanceof Array) {
|
||||||
this.permissions = defaultGroup.permissions;
|
this.permissions = defaultGroup.permissions;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const permissionSet = new Set<string>();
|
const permissionSet = new Set<Permission>();
|
||||||
this.DS.getMany(Group, this.user.groups_id).forEach(group => {
|
this.DS.getMany(Group, this.user.groups_id).forEach(group => {
|
||||||
group.permissions.forEach(permission => {
|
group.permissions.forEach(permission => {
|
||||||
permissionSet.add(permission);
|
permissionSet.add(permission);
|
||||||
@ -416,6 +453,13 @@ export class OperatorService implements OnAfterAppsLoaded {
|
|||||||
this.operatorSubject.next(this.user);
|
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
|
* Returns a default WhoAmI response
|
||||||
*/
|
*/
|
||||||
|
@ -40,7 +40,7 @@ export class PrioritizeService {
|
|||||||
if (this.openSlidesStatusService.isPrioritizedClient !== opPrioritized) {
|
if (this.openSlidesStatusService.isPrioritizedClient !== opPrioritized) {
|
||||||
console.log('Alter prioritization:', opPrioritized);
|
console.log('Alter prioritization:', opPrioritized);
|
||||||
this.openSlidesStatusService.isPrioritizedClient = opPrioritized;
|
this.openSlidesStatusService.isPrioritizedClient = opPrioritized;
|
||||||
this.websocketService.reconnect({ changeId: this.DS.maxChangeId });
|
this.websocketService.reconnect(this.DS.maxChangeId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,11 @@ import { HttpService } from './http.service';
|
|||||||
import { ProjectorDataService } from './projector-data.service';
|
import { ProjectorDataService } from './projector-data.service';
|
||||||
import { ViewModelStoreService } from './view-model-store.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
|
* This service cares about Projectables being projected and manage all projection-related
|
||||||
* actions.
|
* actions.
|
||||||
@ -250,7 +255,7 @@ export class ProjectorService {
|
|||||||
projectorData.forEach(entry => {
|
projectorData.forEach(entry => {
|
||||||
if (entry.data.error && entry.element.stable) {
|
if (entry.data.error && entry.element.stable) {
|
||||||
// Remove this element
|
// Remove this element
|
||||||
const idElementToRemove = this.slideManager.getIdentifialbeProjectorElement(entry.element);
|
const idElementToRemove = this.slideManager.getIdentifiableProjectorElement(entry.element);
|
||||||
elements = elements.filter(element => {
|
elements = elements.filter(element => {
|
||||||
return !elementIdentifies(idElementToRemove, element);
|
return !elementIdentifies(idElementToRemove, element);
|
||||||
});
|
});
|
||||||
@ -325,9 +330,9 @@ export class ProjectorService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
public getSlideTitle(element: ProjectorElement): string {
|
public getSlideTitle(element: ProjectorElement): ProjectorTitle {
|
||||||
if (this.slideManager.canSlideBeMappedToModel(element.name)) {
|
if (this.slideManager.canSlideBeMappedToModel(element.name)) {
|
||||||
const idElement = this.slideManager.getIdentifialbeProjectorElement(element);
|
const idElement = this.slideManager.getIdentifiableProjectorElement(element);
|
||||||
const viewModel = this.getViewModelFromProjectorElement(idElement);
|
const viewModel = this.getViewModelFromProjectorElement(idElement);
|
||||||
if (viewModel) {
|
if (viewModel) {
|
||||||
return viewModel.getProjectorTitle();
|
return viewModel.getProjectorTitle();
|
||||||
@ -338,7 +343,7 @@ export class ProjectorService {
|
|||||||
return configuration.getSlideTitle(element, this.translate, this.viewModelStore);
|
return configuration.getSlideTitle(element, this.translate, this.viewModelStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.translate.instant(this.slideManager.getSlideVerboseName(element.name));
|
return { title: this.translate.instant(this.slideManager.getSlideVerboseName(element.name)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -98,6 +98,17 @@ export class RelationManagerService {
|
|||||||
viewModel: BaseViewModel,
|
viewModel: BaseViewModel,
|
||||||
relation: RelationDefinition
|
relation: RelationDefinition
|
||||||
): any {
|
): any {
|
||||||
|
// No cache for reverse relations.
|
||||||
|
// The issue: we cannot invalidate the cache, if a new object is created (The
|
||||||
|
// following example is for a O2M foreign relation):
|
||||||
|
// There is no possibility to detect the create case: The target does not update,
|
||||||
|
// all related models does not update. The autoupdate does not provide the created-
|
||||||
|
// information. So we may check, if the relaten has changed in length every time. But
|
||||||
|
// this is the same as just resolving the relation every time it is requested. So no cache here.
|
||||||
|
if (isReverseRelationDefinition(relation)) {
|
||||||
|
return this.handleRelation(model, viewModel, relation) as BaseViewModel | BaseViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
let result: any;
|
let result: any;
|
||||||
|
|
||||||
const cacheProperty = '__' + property;
|
const cacheProperty = '__' + property;
|
||||||
@ -187,12 +198,24 @@ export class RelationManagerService {
|
|||||||
const _model: M = target.getModel();
|
const _model: M = target.getModel();
|
||||||
const relation = typeof property === 'string' ? relationsByKey[property] : null;
|
const relation = typeof property === 'string' ? relationsByKey[property] : null;
|
||||||
|
|
||||||
|
// try to find a getter for property
|
||||||
if (property in target) {
|
if (property in target) {
|
||||||
const descriptor = Object.getOwnPropertyDescriptor(viewModelCtor.prototype, property);
|
// iterate over prototype chain
|
||||||
|
let prototypeFunc = viewModelCtor,
|
||||||
|
descriptor = null;
|
||||||
|
do {
|
||||||
|
descriptor = Object.getOwnPropertyDescriptor(prototypeFunc.prototype, property);
|
||||||
|
if (!descriptor || !descriptor.get) {
|
||||||
|
prototypeFunc = Object.getPrototypeOf(prototypeFunc);
|
||||||
|
}
|
||||||
|
} while (!(descriptor && descriptor.get) && prototypeFunc && prototypeFunc.prototype);
|
||||||
|
|
||||||
if (descriptor && descriptor.get) {
|
if (descriptor && descriptor.get) {
|
||||||
|
// if getter was found in prototype chain, bind it with this proxy for right `this` access
|
||||||
result = descriptor.get.bind(viewModel)();
|
result = descriptor.get.bind(viewModel)();
|
||||||
} else {
|
} else {
|
||||||
result = target[property];
|
result = target[property];
|
||||||
|
// console.log(property, target);
|
||||||
}
|
}
|
||||||
} else if (property in _model) {
|
} else if (property in _model) {
|
||||||
result = _model[property];
|
result = _model[property];
|
||||||
|
@ -13,7 +13,7 @@ describe('TimeTravelService', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
const service: TimeTravelService = TestBed.get(TimeTravelService);
|
const service: TimeTravelService = TestBed.inject(TimeTravelService);
|
||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -55,14 +55,6 @@ export const WEBSOCKET_ERROR_CODES = {
|
|||||||
WRONG_FORMAT: 102
|
WRONG_FORMAT: 102
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
* Options for (re-)connecting.
|
|
||||||
*/
|
|
||||||
interface ConnectOptions {
|
|
||||||
changeId?: number;
|
|
||||||
enableAutoupdates?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service that handles WebSocket connections. Other services can register themselfs
|
* Service that handles WebSocket connections. Other services can register themselfs
|
||||||
* with {@method getOberservable} for a specific type of messages. The content will be published.
|
* with {@method getOberservable} for a specific type of messages. The content will be published.
|
||||||
@ -207,10 +199,8 @@ export class WebsocketService {
|
|||||||
*
|
*
|
||||||
* Uses NgZone to let all callbacks run in the angular context.
|
* Uses NgZone to let all callbacks run in the angular context.
|
||||||
*/
|
*/
|
||||||
public async connect(options: ConnectOptions = {}, retry: boolean = false): Promise<void> {
|
public async connect(changeId: number | null = null, retry: boolean = false): Promise<void> {
|
||||||
const websocketId = Math.random()
|
const websocketId = Math.random().toString(36).substring(7);
|
||||||
.toString(36)
|
|
||||||
.substring(7);
|
|
||||||
this.websocketId = websocketId;
|
this.websocketId = websocketId;
|
||||||
|
|
||||||
if (this.websocket) {
|
if (this.websocket) {
|
||||||
@ -222,17 +212,10 @@ export class WebsocketService {
|
|||||||
this.shouldBeClosed = false;
|
this.shouldBeClosed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set defaults
|
const queryParams: QueryParams = {};
|
||||||
options = Object.assign(options, {
|
|
||||||
enableAutoupdates: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryParams: QueryParams = {
|
if (changeId !== null) {
|
||||||
autoupdate: options.enableAutoupdates
|
queryParams.change_id = changeId;
|
||||||
};
|
|
||||||
|
|
||||||
if (options.changeId !== undefined) {
|
|
||||||
queryParams.change_id = options.changeId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the websocket
|
// Create the websocket
|
||||||
@ -316,8 +299,9 @@ export class WebsocketService {
|
|||||||
const compressedSize = data.byteLength;
|
const compressedSize = data.byteLength;
|
||||||
const decompressedBuffer: Uint8Array = decompress(new Uint8Array(data));
|
const decompressedBuffer: Uint8Array = decompress(new Uint8Array(data));
|
||||||
console.debug(
|
console.debug(
|
||||||
`Recieved ${compressedSize / 1024} KB (${decompressedBuffer.byteLength /
|
`Recieved ${compressedSize / 1024} KB (${
|
||||||
1024} KB uncompressed), ratio ${decompressedBuffer.byteLength / compressedSize}`
|
decompressedBuffer.byteLength / 1024
|
||||||
|
} KB uncompressed), ratio ${decompressedBuffer.byteLength / compressedSize}`
|
||||||
);
|
);
|
||||||
data = this.arrayBufferToString(decompressedBuffer);
|
data = this.arrayBufferToString(decompressedBuffer);
|
||||||
}
|
}
|
||||||
@ -399,7 +383,7 @@ export class WebsocketService {
|
|||||||
const timeout = Math.floor(Math.random() * 3000 + 2000);
|
const timeout = Math.floor(Math.random() * 3000 + 2000);
|
||||||
this.retryTimeout = setTimeout(() => {
|
this.retryTimeout = setTimeout(() => {
|
||||||
this.retryTimeout = null;
|
this.retryTimeout = null;
|
||||||
this.connect({ enableAutoupdates: true }, true);
|
this.connect(null, true);
|
||||||
}, timeout);
|
}, timeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -439,9 +423,9 @@ export class WebsocketService {
|
|||||||
*
|
*
|
||||||
* @param options The options for the new connection
|
* @param options The options for the new connection
|
||||||
*/
|
*/
|
||||||
public async reconnect(options: ConnectOptions = {}): Promise<void> {
|
public async reconnect(changeId: number | null = null): Promise<void> {
|
||||||
await this.close();
|
await this.close();
|
||||||
await this.connect(options);
|
await this.connect(changeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2,11 +2,8 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { NgModule, Optional, SkipSelf, Type } from '@angular/core';
|
import { NgModule, Optional, SkipSelf, Type } from '@angular/core';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { ProjectionDialogComponent } from 'app/shared/components/projection-dialog/projection-dialog.component';
|
|
||||||
import { ChoiceDialogComponent } from '../shared/components/choice-dialog/choice-dialog.component';
|
|
||||||
import { OnAfterAppsLoaded } from './definitions/on-after-apps-loaded';
|
import { OnAfterAppsLoaded } from './definitions/on-after-apps-loaded';
|
||||||
import { OperatorService } from './core-services/operator.service';
|
import { OperatorService } from './core-services/operator.service';
|
||||||
import { PromptDialogComponent } from '../shared/components/prompt-dialog/prompt-dialog.component';
|
|
||||||
|
|
||||||
export const ServicesToLoadOnAppsLoaded: Type<OnAfterAppsLoaded>[] = [OperatorService];
|
export const ServicesToLoadOnAppsLoaded: Type<OnAfterAppsLoaded>[] = [OperatorService];
|
||||||
|
|
||||||
@ -15,8 +12,7 @@ export const ServicesToLoadOnAppsLoaded: Type<OnAfterAppsLoaded>[] = [OperatorSe
|
|||||||
*/
|
*/
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [CommonModule],
|
imports: [CommonModule],
|
||||||
providers: [Title],
|
providers: [Title]
|
||||||
entryComponents: [PromptDialogComponent, ChoiceDialogComponent, ProjectionDialogComponent]
|
|
||||||
})
|
})
|
||||||
export class CoreModule {
|
export class CoreModule {
|
||||||
/** make sure CoreModule is imported only by one NgModule, the AppModule */
|
/** make sure CoreModule is imported only by one NgModule, the AppModule */
|
||||||
|
@ -7,7 +7,6 @@ import { MainMenuEntry } from '../core-services/main-menu.service';
|
|||||||
import { Searchable } from '../../site/base/searchable';
|
import { Searchable } from '../../site/base/searchable';
|
||||||
|
|
||||||
interface BaseModelEntry {
|
interface BaseModelEntry {
|
||||||
collectionString: string;
|
|
||||||
repository: Type<BaseRepository<any, any, any>>;
|
repository: Type<BaseRepository<any, any, any>>;
|
||||||
model: ModelConstructor<BaseModel>;
|
model: ModelConstructor<BaseModel>;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
export interface HasViewModelListObservable<V> {
|
||||||
|
getViewModelListObservable(): Observable<V[]>;
|
||||||
|
}
|
@ -618,10 +618,7 @@ export class HtmlToPdfService {
|
|||||||
const styleObject: any = {};
|
const styleObject: any = {};
|
||||||
if (styles && styles.length > 0) {
|
if (styles && styles.length > 0) {
|
||||||
for (const style of styles) {
|
for (const style of styles) {
|
||||||
const styleDefinition = style
|
const styleDefinition = style.trim().toLowerCase().split(':');
|
||||||
.trim()
|
|
||||||
.toLowerCase()
|
|
||||||
.split(':');
|
|
||||||
const key = styleDefinition[0];
|
const key = styleDefinition[0];
|
||||||
const value = styleDefinition[1];
|
const value = styleDefinition[1];
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { HttpHeaders } from '@angular/common/http';
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { MatSnackBar } from '@angular/material';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
@ -47,8 +47,8 @@ export class PdfError extends Error {
|
|||||||
* Provides the general document structure for PDF documents, such as page margins, header, footer and styles.
|
* Provides the general document structure for PDF documents, such as page margins, header, footer and styles.
|
||||||
* Also provides general purpose open and download functions.
|
* Also provides general purpose open and download functions.
|
||||||
*
|
*
|
||||||
* Use a local pdf service (i.e. MotionPdfService) to get the document definition for the content and use this service to
|
* Use a local pdf service (i.e. MotionPdfService) to get the document definition for the content and
|
||||||
* open or download the pdf document
|
* use this service to open or download the pdf document
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
@ -256,14 +256,11 @@ export class PdfDocumentService {
|
|||||||
if (logoHeaderLeftUrl && logoHeaderRightUrl) {
|
if (logoHeaderLeftUrl && logoHeaderRightUrl) {
|
||||||
text = '';
|
text = '';
|
||||||
} else {
|
} else {
|
||||||
const general_event_name = this.configService.instant<string>('general_event_name');
|
const general_event_name = this.translate.instant(this.configService.instant<string>('general_event_name'));
|
||||||
const general_event_description = this.configService.instant<string>('general_event_description');
|
const general_event_description = this.translate.instant(
|
||||||
const line1 = [
|
this.configService.instant<string>('general_event_description')
|
||||||
this.translate.instant(general_event_name),
|
);
|
||||||
this.translate.instant(general_event_description)
|
const line1 = [general_event_name, general_event_description].filter(Boolean).join(' - ');
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' – ');
|
|
||||||
const line2 = [
|
const line2 = [
|
||||||
this.configService.instant('general_event_location'),
|
this.configService.instant('general_event_location'),
|
||||||
this.configService.instant('general_event_date')
|
this.configService.instant('general_event_date')
|
||||||
@ -712,6 +709,13 @@ export class PdfDocumentService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getSpacer(): Object {
|
||||||
|
return {
|
||||||
|
text: '',
|
||||||
|
margin: [0, 10]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the table definition for the TOC
|
* Generates the table definition for the TOC
|
||||||
*
|
*
|
||||||
|
@ -77,10 +77,14 @@ function addPageNumbers(data: any): void {
|
|||||||
|
|
||||||
data.doc.footer = (currentPage, pageCount) => {
|
data.doc.footer = (currentPage, pageCount) => {
|
||||||
const footer = data.doc.tmpfooter;
|
const footer = data.doc.tmpfooter;
|
||||||
|
|
||||||
|
// if the tmpfooter starts with an image, the pagenumber will be found in column 1
|
||||||
|
const pageNumberColIndex = !!footer.columns[0].image ? 1 : 0;
|
||||||
|
|
||||||
// "%PAGENR% needs to be found once. After that, the same position should always update page numbers"
|
// "%PAGENR% needs to be found once. After that, the same position should always update page numbers"
|
||||||
if (footer.columns[0].stack[0] === '%PAGENR%' || countPageNumbers) {
|
if (footer.columns[pageNumberColIndex]?.stack[0] === '%PAGENR%' || countPageNumbers) {
|
||||||
countPageNumbers = true;
|
countPageNumbers = true;
|
||||||
footer.columns[0].stack[0] = `${currentPage} / ${pageCount}`;
|
footer.columns[pageNumberColIndex].stack[0] = `${currentPage} / ${pageCount}`;
|
||||||
}
|
}
|
||||||
return footer;
|
return footer;
|
||||||
};
|
};
|
||||||
|
30
client/src/app/core/promises/mutex.ts
Normal file
30
client/src/app/core/promises/mutex.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* A mutex as described in every textbook
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```
|
||||||
|
* mutex = new Mutex(); // create e.g. as class member
|
||||||
|
*
|
||||||
|
* // Somewhere in the code to lock (must be async code!)
|
||||||
|
* const unlock = await this.mutex.lock()
|
||||||
|
* // ...the code to synchronize
|
||||||
|
* unlock()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class Mutex {
|
||||||
|
private mutex = Promise.resolve();
|
||||||
|
|
||||||
|
public lock(): PromiseLike<() => void> {
|
||||||
|
// this will capture the code-to-synchronize
|
||||||
|
let begin: (unlock: () => void) => void = () => {};
|
||||||
|
|
||||||
|
// All "requests" to execute code are chained in a promise-chain
|
||||||
|
this.mutex = this.mutex.then(() => {
|
||||||
|
return new Promise(begin);
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise(res => {
|
||||||
|
begin = res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -14,11 +14,13 @@ import { Identifiable } from 'app/shared/models/base/identifiable';
|
|||||||
import { ItemTitleInformation, ViewItem } from 'app/site/agenda/models/view-item';
|
import { ItemTitleInformation, ViewItem } from 'app/site/agenda/models/view-item';
|
||||||
import { ViewAssignment } from 'app/site/assignments/models/view-assignment';
|
import { ViewAssignment } from 'app/site/assignments/models/view-assignment';
|
||||||
import {
|
import {
|
||||||
|
AgendaListTitle,
|
||||||
BaseViewModelWithAgendaItem,
|
BaseViewModelWithAgendaItem,
|
||||||
isBaseViewModelWithAgendaItem
|
isBaseViewModelWithAgendaItem
|
||||||
} from 'app/site/base/base-view-model-with-agenda-item';
|
} from 'app/site/base/base-view-model-with-agenda-item';
|
||||||
import { ViewMotion } from 'app/site/motions/models/view-motion';
|
import { ViewMotion } from 'app/site/motions/models/view-motion';
|
||||||
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
|
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
|
||||||
|
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||||
import { ViewTopic } from 'app/site/topics/models/view-topic';
|
import { ViewTopic } from 'app/site/topics/models/view-topic';
|
||||||
import { BaseHasContentObjectRepository } from '../base-has-content-object-repository';
|
import { BaseHasContentObjectRepository } from '../base-has-content-object-repository';
|
||||||
import { BaseIsAgendaItemContentObjectRepository } from '../base-is-agenda-item-content-object-repository';
|
import { BaseIsAgendaItemContentObjectRepository } from '../base-is-agenda-item-content-object-repository';
|
||||||
@ -33,6 +35,12 @@ const ItemRelations: RelationDefinition[] = [
|
|||||||
VForeignVerbose: 'BaseViewModelWithAgendaItem',
|
VForeignVerbose: 'BaseViewModelWithAgendaItem',
|
||||||
ownContentObjectDataKey: 'contentObjectData',
|
ownContentObjectDataKey: 'contentObjectData',
|
||||||
ownKey: 'contentObject'
|
ownKey: 'contentObject'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'M2M',
|
||||||
|
ownIdKey: 'tags_id',
|
||||||
|
ownKey: 'tags',
|
||||||
|
foreignViewModel: ViewTag
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -79,7 +87,7 @@ export class ItemRepositoryService extends BaseHasContentObjectRepository<
|
|||||||
return this.translate.instant(plural ? 'Items' : 'Item');
|
return this.translate.instant(plural ? 'Items' : 'Item');
|
||||||
};
|
};
|
||||||
|
|
||||||
public getTitle = (titleInformation: ItemTitleInformation) => {
|
private getAgendaTitle(titleInformation: ItemTitleInformation): AgendaListTitle {
|
||||||
if (titleInformation.contentObject) {
|
if (titleInformation.contentObject) {
|
||||||
return titleInformation.contentObject.getAgendaListTitle();
|
return titleInformation.contentObject.getAgendaListTitle();
|
||||||
} else {
|
} else {
|
||||||
@ -88,36 +96,14 @@ export class ItemRepositoryService extends BaseHasContentObjectRepository<
|
|||||||
) as BaseIsAgendaItemContentObjectRepository<any, any, any>;
|
) as BaseIsAgendaItemContentObjectRepository<any, any, any>;
|
||||||
return repo.getAgendaListTitle(titleInformation.title_information);
|
return repo.getAgendaListTitle(titleInformation.title_information);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTitle = (titleInformation: ItemTitleInformation) => {
|
||||||
|
return this.getAgendaTitle(titleInformation).title;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
public getSubtitle = (titleInformation: ItemTitleInformation) => {
|
||||||
* Overrides the base function, if implemented.
|
return this.getAgendaTitle(titleInformation).subtitle;
|
||||||
*
|
|
||||||
* @returns An optional subtitle as `string`. Defaults to `null`.
|
|
||||||
*/
|
|
||||||
public getSubtitle = (viewItem: ViewItem) => {
|
|
||||||
if (viewItem.contentObject) {
|
|
||||||
return viewItem.contentObject.getAgendaSubtitle();
|
|
||||||
} else {
|
|
||||||
// The subtitle is not present in the title_information yet.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Overrides the base function.
|
|
||||||
*
|
|
||||||
* @returns The title without any prefix like item number.
|
|
||||||
*/
|
|
||||||
public getTitleWithoutItemNumber = (titleInformation: ItemTitleInformation) => {
|
|
||||||
if (titleInformation.contentObject) {
|
|
||||||
return titleInformation.contentObject.getAgendaListTitleWithoutItemNumber();
|
|
||||||
} else {
|
|
||||||
const repo = this.collectionStringMapperService.getRepository(
|
|
||||||
titleInformation.contentObjectData.collection
|
|
||||||
) as BaseIsAgendaItemContentObjectRepository<any, any, any>;
|
|
||||||
return repo.getAgendaListTitleWithoutItemNumber(titleInformation.title_information);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -62,6 +62,17 @@ const ListOfSpeakersNestedModelDescriptors: NestedModelDescriptors = {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object, that contains information about structure-level,
|
||||||
|
* speaking-time and finished-speakers.
|
||||||
|
* Helpful to get a relation between speakers and their structure-level.
|
||||||
|
*/
|
||||||
|
export interface SpeakingTimeStructureLevelObject {
|
||||||
|
structureLevel: string;
|
||||||
|
finishedSpeakers: ViewSpeaker[];
|
||||||
|
speakingTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository service for lists of speakers
|
* Repository service for lists of speakers
|
||||||
*
|
*
|
||||||
@ -169,8 +180,8 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit
|
|||||||
/**
|
/**
|
||||||
* Posts an (manually) sorted speaker list to the server
|
* Posts an (manually) sorted speaker list to the server
|
||||||
*
|
*
|
||||||
|
* @param listOfSpeakers the target list of speakers, which speaker-list is changed.
|
||||||
* @param speakerIds array of speaker id numbers
|
* @param speakerIds array of speaker id numbers
|
||||||
* @param Item the target agenda item
|
|
||||||
*/
|
*/
|
||||||
public async sortSpeakers(listOfSpeakers: ViewListOfSpeakers, speakerIds: number[]): Promise<void> {
|
public async sortSpeakers(listOfSpeakers: ViewListOfSpeakers, speakerIds: number[]): Promise<void> {
|
||||||
const restUrl = this.getRestUrl(listOfSpeakers.id, 'sort_speakers');
|
const restUrl = this.getRestUrl(listOfSpeakers.id, 'sort_speakers');
|
||||||
@ -220,6 +231,101 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit
|
|||||||
await this.httpService.put(restUrl, { speaker: speaker.id });
|
await this.httpService.put(restUrl, { speaker: speaker.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async deleteAllSpeakersOfAllListsOfSpeakers(): Promise<void> {
|
||||||
|
await this.httpService.post('/rest/agenda/list-of-speakers/delete_all_speakers/');
|
||||||
|
}
|
||||||
|
|
||||||
|
public isFirstContribution(speaker: ViewSpeaker): boolean {
|
||||||
|
return !this.getViewModelList().some(list => list.hasSpeakerSpoken(speaker));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List every speaker only once, who has spoken
|
||||||
|
*
|
||||||
|
* @returns A list with all different speakers.
|
||||||
|
*/
|
||||||
|
public getAllFirstContributions(): ViewSpeaker[] {
|
||||||
|
const speakers: ViewSpeaker[] = this.getViewModelList().flatMap(
|
||||||
|
(los: ViewListOfSpeakers) => los.finishedSpeakers
|
||||||
|
);
|
||||||
|
const firstContributions: ViewSpeaker[] = [];
|
||||||
|
for (const speaker of speakers) {
|
||||||
|
if (!firstContributions.find(s => s.user_id === speaker.user_id)) {
|
||||||
|
firstContributions.push(speaker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return firstContributions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps structure-level to speaker.
|
||||||
|
*
|
||||||
|
* @returns A list, which entries are `SpeakingTimeStructureLevelObject`.
|
||||||
|
*/
|
||||||
|
public getSpeakingTimeStructureLevelRelation(): SpeakingTimeStructureLevelObject[] {
|
||||||
|
let listSpeakingTimeStructureLevel: SpeakingTimeStructureLevelObject[] = [];
|
||||||
|
for (const los of this.getViewModelList()) {
|
||||||
|
for (const speaker of los.finishedSpeakers) {
|
||||||
|
const nextEntry = this.getSpeakingTimeStructureLevelObject(speaker);
|
||||||
|
listSpeakingTimeStructureLevel = this.getSpeakingTimeStructureLevelList(
|
||||||
|
nextEntry,
|
||||||
|
listSpeakingTimeStructureLevel
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listSpeakingTimeStructureLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper-function to create a `SpeakingTimeStructureLevelObject` by a given speaker.
|
||||||
|
*
|
||||||
|
* @param speaker, with whom structure-level and speaking-time is calculated.
|
||||||
|
*
|
||||||
|
* @returns The created `SpeakingTimeStructureLevelObject`.
|
||||||
|
*/
|
||||||
|
private getSpeakingTimeStructureLevelObject(speaker: ViewSpeaker): SpeakingTimeStructureLevelObject {
|
||||||
|
return {
|
||||||
|
structureLevel:
|
||||||
|
!speaker.user || (speaker.user && !speaker.user.structure_level) ? '–' : speaker.user.structure_level,
|
||||||
|
finishedSpeakers: [speaker],
|
||||||
|
speakingTime: this.getSpeakingTimeAsNumber(speaker)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper-function to update entries in a given list, if already existing, or create entries otherwise.
|
||||||
|
*
|
||||||
|
* @param object A `SpeakingTimeStructureLevelObject`, that contains information about speaking-time
|
||||||
|
* and structure-level.
|
||||||
|
* @param list A list, at which speaking-time, structure-level and finished_speakers are set.
|
||||||
|
*
|
||||||
|
* @returns The updated map.
|
||||||
|
*/
|
||||||
|
private getSpeakingTimeStructureLevelList(
|
||||||
|
object: SpeakingTimeStructureLevelObject,
|
||||||
|
list: SpeakingTimeStructureLevelObject[]
|
||||||
|
): SpeakingTimeStructureLevelObject[] {
|
||||||
|
const index = list.findIndex(entry => entry.structureLevel === object.structureLevel);
|
||||||
|
if (index >= 0) {
|
||||||
|
list[index].speakingTime += object.speakingTime;
|
||||||
|
list[index].finishedSpeakers.push(...object.finishedSpeakers);
|
||||||
|
} else {
|
||||||
|
list.push(object);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function calculates speaking-time as number for a given speaker.
|
||||||
|
*
|
||||||
|
* @param speaker The speaker, whose speaking-time should be calculated.
|
||||||
|
*
|
||||||
|
* @returns A number, that represents the speaking-time.
|
||||||
|
*/
|
||||||
|
private getSpeakingTimeAsNumber(speaker: ViewSpeaker): number {
|
||||||
|
return Math.floor((new Date(speaker.end_time).valueOf() - new Date(speaker.begin_time).valueOf()) / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function get the url to the speaker rest address
|
* Helper function get the url to the speaker rest address
|
||||||
*
|
*
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { AssignmentOptionRepositoryService } from './assignment-option-repository.service';
|
||||||
|
|
||||||
|
describe('AssignmentOptionRepositoryService', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: AssignmentOptionRepositoryService = TestBed.inject(AssignmentOptionRepositoryService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,75 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { DataSendService } from 'app/core/core-services/data-send.service';
|
||||||
|
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
||||||
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
|
import { RelationDefinition } from 'app/core/definitions/relations';
|
||||||
|
import { AssignmentOption } from 'app/shared/models/assignments/assignment-option';
|
||||||
|
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
|
||||||
|
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||||
|
import { ViewAssignmentVote } from 'app/site/assignments/models/view-assignment-vote';
|
||||||
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
|
import { BaseRepository } from '../base-repository';
|
||||||
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
|
|
||||||
|
const AssignmentOptionRelations: RelationDefinition[] = [
|
||||||
|
{
|
||||||
|
type: 'O2M',
|
||||||
|
foreignIdKey: 'option_id',
|
||||||
|
ownKey: 'votes',
|
||||||
|
foreignViewModel: ViewAssignmentVote
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'M2O',
|
||||||
|
ownIdKey: 'poll_id',
|
||||||
|
ownKey: 'poll',
|
||||||
|
foreignViewModel: ViewAssignmentPoll
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'M2O',
|
||||||
|
ownIdKey: 'user_id',
|
||||||
|
ownKey: 'user',
|
||||||
|
foreignViewModel: ViewUser
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository Service for Options.
|
||||||
|
*
|
||||||
|
* Documentation partially provided in {@link BaseRepository}
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AssignmentOptionRepositoryService extends BaseRepository<ViewAssignmentOption, AssignmentOption, object> {
|
||||||
|
public constructor(
|
||||||
|
DS: DataStoreService,
|
||||||
|
dataSend: DataSendService,
|
||||||
|
mapperService: CollectionStringMapperService,
|
||||||
|
viewModelStoreService: ViewModelStoreService,
|
||||||
|
translate: TranslateService,
|
||||||
|
relationManager: RelationManagerService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
DS,
|
||||||
|
dataSend,
|
||||||
|
mapperService,
|
||||||
|
viewModelStoreService,
|
||||||
|
translate,
|
||||||
|
relationManager,
|
||||||
|
AssignmentOption,
|
||||||
|
AssignmentOptionRelations
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTitle = (titleInformation: object) => {
|
||||||
|
return 'Option';
|
||||||
|
};
|
||||||
|
|
||||||
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
|
return this.translate.instant(plural ? 'Options' : 'Option');
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { AssignmentPollRepositoryService } from './assignment-poll-repository.service';
|
||||||
|
|
||||||
|
describe('AssignmentPollRepositoryService', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: AssignmentPollRepositoryService = TestBed.inject(AssignmentPollRepositoryService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,136 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { DataSendService } from 'app/core/core-services/data-send.service';
|
||||||
|
import { HttpService } from 'app/core/core-services/http.service';
|
||||||
|
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
||||||
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
|
import { RelationDefinition } from 'app/core/definitions/relations';
|
||||||
|
import { VotingService } from 'app/core/ui-services/voting.service';
|
||||||
|
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
||||||
|
import { ViewAssignment } from 'app/site/assignments/models/view-assignment';
|
||||||
|
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
|
||||||
|
import { AssignmentPollTitleInformation, ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||||
|
import { BasePollRepositoryService } from 'app/site/polls/services/base-poll-repository.service';
|
||||||
|
import { ViewGroup } from 'app/site/users/models/view-group';
|
||||||
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
|
|
||||||
|
const AssignmentPollRelations: RelationDefinition[] = [
|
||||||
|
{
|
||||||
|
type: 'M2M',
|
||||||
|
ownIdKey: 'groups_id',
|
||||||
|
ownKey: 'groups',
|
||||||
|
foreignViewModel: ViewGroup
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'O2M',
|
||||||
|
ownIdKey: 'options_id',
|
||||||
|
ownKey: 'options',
|
||||||
|
foreignViewModel: ViewAssignmentOption
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'M2O',
|
||||||
|
ownIdKey: 'assignment_id',
|
||||||
|
ownKey: 'assignment',
|
||||||
|
foreignViewModel: ViewAssignment
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'M2M',
|
||||||
|
ownIdKey: 'voted_id',
|
||||||
|
ownKey: 'voted',
|
||||||
|
foreignViewModel: ViewUser
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface AssignmentAnalogVoteData {
|
||||||
|
options: {
|
||||||
|
[key: number]: {
|
||||||
|
Y: number;
|
||||||
|
N?: number;
|
||||||
|
A?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
votesvalid?: number;
|
||||||
|
votesinvalid?: number;
|
||||||
|
votescast?: number;
|
||||||
|
global_no?: number;
|
||||||
|
global_abstain?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VotingData {
|
||||||
|
votes: Object;
|
||||||
|
global?: GlobalVote;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GlobalVote = 'A' | 'N';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository Service for Assignments.
|
||||||
|
*
|
||||||
|
* Documentation partially provided in {@link BaseRepository}
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AssignmentPollRepositoryService extends BasePollRepositoryService<
|
||||||
|
ViewAssignmentPoll,
|
||||||
|
AssignmentPoll,
|
||||||
|
AssignmentPollTitleInformation
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* Constructor for the Assignment Repository.
|
||||||
|
*
|
||||||
|
* @param DS DataStore access
|
||||||
|
* @param dataSend Sending data
|
||||||
|
* @param mapperService Map models to object
|
||||||
|
* @param viewModelStoreService Access view models
|
||||||
|
* @param translate Translate string
|
||||||
|
* @param httpService make HTTP Requests
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
DS: DataStoreService,
|
||||||
|
dataSend: DataSendService,
|
||||||
|
mapperService: CollectionStringMapperService,
|
||||||
|
viewModelStoreService: ViewModelStoreService,
|
||||||
|
translate: TranslateService,
|
||||||
|
relationManager: RelationManagerService,
|
||||||
|
votingService: VotingService,
|
||||||
|
http: HttpService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
DS,
|
||||||
|
dataSend,
|
||||||
|
mapperService,
|
||||||
|
viewModelStoreService,
|
||||||
|
translate,
|
||||||
|
relationManager,
|
||||||
|
AssignmentPoll,
|
||||||
|
AssignmentPollRelations,
|
||||||
|
{},
|
||||||
|
votingService,
|
||||||
|
http
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTitle = (titleInformation: AssignmentPollTitleInformation) => {
|
||||||
|
return titleInformation.title;
|
||||||
|
};
|
||||||
|
|
||||||
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
|
return this.translate.instant(plural ? 'Polls' : 'Poll');
|
||||||
|
};
|
||||||
|
|
||||||
|
public vote(data: VotingData, poll_id: number): Promise<void> {
|
||||||
|
let requestData;
|
||||||
|
if (data.global) {
|
||||||
|
requestData = `"${data.global}"`;
|
||||||
|
} else {
|
||||||
|
requestData = data.votes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.post(`/rest/assignments/assignment-poll/${poll_id}/vote/`, requestData);
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,7 @@ describe('AssignmentRepositoryService', () => {
|
|||||||
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
const service: AssignmentRepositoryService = TestBed.get(AssignmentRepositoryService);
|
const service: AssignmentRepositoryService = TestBed.inject(AssignmentRepositoryService);
|
||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -8,12 +8,9 @@ import { RelationManagerService } from 'app/core/core-services/relation-manager.
|
|||||||
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
import { RelationDefinition } from 'app/core/definitions/relations';
|
import { RelationDefinition } from 'app/core/definitions/relations';
|
||||||
import { Assignment } from 'app/shared/models/assignments/assignment';
|
import { Assignment } from 'app/shared/models/assignments/assignment';
|
||||||
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
|
|
||||||
import { AssignmentPollOption } from 'app/shared/models/assignments/assignment-poll-option';
|
|
||||||
import { AssignmentRelatedUser } from 'app/shared/models/assignments/assignment-related-user';
|
import { AssignmentRelatedUser } from 'app/shared/models/assignments/assignment-related-user';
|
||||||
import { AssignmentTitleInformation, ViewAssignment } from 'app/site/assignments/models/view-assignment';
|
import { AssignmentTitleInformation, ViewAssignment } from 'app/site/assignments/models/view-assignment';
|
||||||
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||||
import { ViewAssignmentPollOption } from 'app/site/assignments/models/view-assignment-poll-option';
|
|
||||||
import { ViewAssignmentRelatedUser } from 'app/site/assignments/models/view-assignment-related-user';
|
import { ViewAssignmentRelatedUser } from 'app/site/assignments/models/view-assignment-related-user';
|
||||||
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
||||||
import { ViewTag } from 'app/site/tags/models/view-tag';
|
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||||
@ -35,6 +32,12 @@ const AssignmentRelations: RelationDefinition[] = [
|
|||||||
ownIdKey: 'attachments_id',
|
ownIdKey: 'attachments_id',
|
||||||
ownKey: 'attachments',
|
ownKey: 'attachments',
|
||||||
foreignViewModel: ViewMediafile
|
foreignViewModel: ViewMediafile
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'O2M',
|
||||||
|
ownKey: 'polls',
|
||||||
|
foreignIdKey: 'assignment_id',
|
||||||
|
foreignViewModel: ViewAssignmentPoll
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -57,28 +60,6 @@ const AssignmentNestedModelDescriptors: NestedModelDescriptors = {
|
|||||||
getTitle: (viewAssignmentRelatedUser: ViewAssignmentRelatedUser) =>
|
getTitle: (viewAssignmentRelatedUser: ViewAssignmentRelatedUser) =>
|
||||||
viewAssignmentRelatedUser.user ? viewAssignmentRelatedUser.user.getFullName() : ''
|
viewAssignmentRelatedUser.user ? viewAssignmentRelatedUser.user.getFullName() : ''
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
ownKey: 'polls',
|
|
||||||
foreignViewModel: ViewAssignmentPoll,
|
|
||||||
foreignModel: AssignmentPoll,
|
|
||||||
relationDefinitionsByKey: {}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'assignments/assignment-poll': [
|
|
||||||
{
|
|
||||||
ownKey: 'options',
|
|
||||||
foreignViewModel: ViewAssignmentPollOption,
|
|
||||||
foreignModel: AssignmentPollOption,
|
|
||||||
order: 'weight',
|
|
||||||
relationDefinitionsByKey: {
|
|
||||||
user: {
|
|
||||||
type: 'M2O',
|
|
||||||
ownIdKey: 'candidate_id',
|
|
||||||
ownKey: 'user',
|
|
||||||
foreignViewModel: ViewUser
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@ -97,11 +78,8 @@ export class AssignmentRepositoryService extends BaseIsAgendaItemAndListOfSpeake
|
|||||||
AssignmentTitleInformation
|
AssignmentTitleInformation
|
||||||
> {
|
> {
|
||||||
private readonly restPath = '/rest/assignments/assignment/';
|
private readonly restPath = '/rest/assignments/assignment/';
|
||||||
private readonly restPollPath = '/rest/assignments/poll/';
|
|
||||||
private readonly candidatureOtherPath = '/candidature_other/';
|
private readonly candidatureOtherPath = '/candidature_other/';
|
||||||
private readonly candidatureSelfPath = '/candidature_self/';
|
private readonly candidatureSelfPath = '/candidature_self/';
|
||||||
private readonly createPollPath = '/create_poll/';
|
|
||||||
private readonly markElectedPath = '/mark_elected/';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for the Assignment Repository.
|
* Constructor for the Assignment Repository.
|
||||||
@ -179,87 +157,6 @@ export class AssignmentRepositoryService extends BaseIsAgendaItemAndListOfSpeake
|
|||||||
await this.httpService.delete(this.restPath + assignment.id + this.candidatureSelfPath);
|
await this.httpService.delete(this.restPath + assignment.id + this.candidatureSelfPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new Poll to a given assignment
|
|
||||||
*
|
|
||||||
* @param assignment The assignment to add the poll to
|
|
||||||
*/
|
|
||||||
public async addPoll(assignment: ViewAssignment): Promise<void> {
|
|
||||||
await this.httpService.post(this.restPath + assignment.id + this.createPollPath);
|
|
||||||
// TODO: change current tab to new poll
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a poll
|
|
||||||
*
|
|
||||||
* @param id id of the poll to delete
|
|
||||||
*/
|
|
||||||
public async deletePoll(poll: ViewAssignmentPoll): Promise<void> {
|
|
||||||
await this.httpService.delete(`${this.restPollPath}${poll.id}/`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* update data (metadata etc) for a poll
|
|
||||||
*
|
|
||||||
* @param poll the (partial) data to update
|
|
||||||
* @param originalPoll the poll to update
|
|
||||||
*
|
|
||||||
* TODO: check if votes is untouched
|
|
||||||
*/
|
|
||||||
public async updatePoll(poll: Partial<AssignmentPoll>, originalPoll: ViewAssignmentPoll): Promise<void> {
|
|
||||||
const data: AssignmentPoll = Object.assign(originalPoll.poll, poll);
|
|
||||||
await this.httpService.patch(`${this.restPollPath}${originalPoll.id}/`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: temporary (?) update votes method. Needed because server needs
|
|
||||||
* different input than it's output in case of votes ?
|
|
||||||
*
|
|
||||||
* @param poll the updated Poll
|
|
||||||
* @param originalPoll the original poll
|
|
||||||
*/
|
|
||||||
public async updateVotes(poll: Partial<AssignmentPoll>, originalPoll: ViewAssignmentPoll): Promise<void> {
|
|
||||||
const votes = poll.options.map(option => {
|
|
||||||
const voteObject = {};
|
|
||||||
for (const vote of option.votes) {
|
|
||||||
voteObject[vote.value] = vote.weight;
|
|
||||||
}
|
|
||||||
return voteObject;
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
assignment_id: originalPoll.assignment_id,
|
|
||||||
votes: votes,
|
|
||||||
votesabstain: poll.votesabstain || null,
|
|
||||||
votescast: poll.votescast || null,
|
|
||||||
votesinvalid: poll.votesinvalid || null,
|
|
||||||
votesno: poll.votesno || null,
|
|
||||||
votesvalid: poll.votesvalid || null
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.httpService.put(`${this.restPollPath}${originalPoll.id}/`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* change the 'elected' state of an election candidate
|
|
||||||
*
|
|
||||||
* @param assignmentRelatedUser
|
|
||||||
* @param assignment
|
|
||||||
* @param elected true if the candidate is to be elected, false if unelected
|
|
||||||
*/
|
|
||||||
public async markElected(
|
|
||||||
assignmentRelatedUser: ViewAssignmentRelatedUser,
|
|
||||||
assignment: ViewAssignment,
|
|
||||||
elected: boolean
|
|
||||||
): Promise<void> {
|
|
||||||
const data = { user: assignmentRelatedUser.user_id };
|
|
||||||
if (elected) {
|
|
||||||
await this.httpService.post(this.restPath + assignment.id + this.markElectedPath, data);
|
|
||||||
} else {
|
|
||||||
await this.httpService.delete(this.restPath + assignment.id + this.markElectedPath, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a request to sort an assignment's candidates
|
* Sends a request to sort an assignment's candidates
|
||||||
*
|
*
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { AssignmentVoteRepositoryService } from './assignment-vote-repository.service';
|
||||||
|
|
||||||
|
describe('AssignmentVoteRepositoryService', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: AssignmentVoteRepositoryService = TestBed.inject(AssignmentVoteRepositoryService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,80 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { DataSendService } from 'app/core/core-services/data-send.service';
|
||||||
|
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
||||||
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
|
import { RelationDefinition } from 'app/core/definitions/relations';
|
||||||
|
import { AssignmentVote } from 'app/shared/models/assignments/assignment-vote';
|
||||||
|
import { ViewAssignmentOption } from 'app/site/assignments/models/view-assignment-option';
|
||||||
|
import { ViewAssignmentVote } from 'app/site/assignments/models/view-assignment-vote';
|
||||||
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
|
import { BaseRepository } from '../base-repository';
|
||||||
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
|
|
||||||
|
const AssignmentVoteRelations: RelationDefinition[] = [
|
||||||
|
{
|
||||||
|
type: 'M2O',
|
||||||
|
ownIdKey: 'user_id',
|
||||||
|
ownKey: 'user',
|
||||||
|
foreignViewModel: ViewUser
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'M2O',
|
||||||
|
ownIdKey: 'option_id',
|
||||||
|
ownKey: 'option',
|
||||||
|
foreignViewModel: ViewAssignmentOption
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository Service for Assignments.
|
||||||
|
*
|
||||||
|
* Documentation partially provided in {@link BaseRepository}
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AssignmentVoteRepositoryService extends BaseRepository<ViewAssignmentVote, AssignmentVote, object> {
|
||||||
|
/**
|
||||||
|
* @param DS DataStore access
|
||||||
|
* @param dataSend Sending data
|
||||||
|
* @param mapperService Map models to object
|
||||||
|
* @param viewModelStoreService Access view models
|
||||||
|
* @param translate Translate string
|
||||||
|
* @param httpService make HTTP Requests
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
DS: DataStoreService,
|
||||||
|
dataSend: DataSendService,
|
||||||
|
mapperService: CollectionStringMapperService,
|
||||||
|
viewModelStoreService: ViewModelStoreService,
|
||||||
|
translate: TranslateService,
|
||||||
|
relationManager: RelationManagerService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
DS,
|
||||||
|
dataSend,
|
||||||
|
mapperService,
|
||||||
|
viewModelStoreService,
|
||||||
|
translate,
|
||||||
|
relationManager,
|
||||||
|
AssignmentVote,
|
||||||
|
AssignmentVoteRelations
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTitle = (titleInformation: object) => {
|
||||||
|
return 'Vote';
|
||||||
|
};
|
||||||
|
|
||||||
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
|
return this.translate.instant(plural ? 'Votes' : 'Vote');
|
||||||
|
};
|
||||||
|
|
||||||
|
public getVotesForUser(pollId: number, userId: number): ViewAssignmentVote[] {
|
||||||
|
return this.getViewModelList().filter(vote => vote.option.poll_id === pollId && vote.user_id === userId);
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ import { ViewItem } from 'app/site/agenda/models/view-item';
|
|||||||
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
|
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
|
||||||
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
|
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
|
||||||
import {
|
import {
|
||||||
|
AgendaListTitle,
|
||||||
BaseViewModelWithAgendaItem,
|
BaseViewModelWithAgendaItem,
|
||||||
TitleInformationWithAgendaItem
|
TitleInformationWithAgendaItem
|
||||||
} from 'app/site/base/base-view-model-with-agenda-item';
|
} from 'app/site/base/base-view-model-with-agenda-item';
|
||||||
@ -52,14 +53,11 @@ export abstract class BaseIsAgendaItemAndListOfSpeakersContentObjectRepository<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAgendaListTitle(titleInformation: T): string {
|
public getAgendaListTitle(titleInformation: T): AgendaListTitle {
|
||||||
// Return the agenda title with the model's verbose name appended
|
// Return the agenda title with the model's verbose name appended
|
||||||
const numberPrefix = titleInformation.agenda_item_number() ? `${titleInformation.agenda_item_number()} · ` : '';
|
const numberPrefix = titleInformation.agenda_item_number() ? `${titleInformation.agenda_item_number()} · ` : '';
|
||||||
return numberPrefix + this.getTitle(titleInformation) + ' (' + this.getVerboseName() + ')';
|
const title = numberPrefix + this.getTitle(titleInformation) + ' (' + this.getVerboseName() + ')';
|
||||||
}
|
return { title };
|
||||||
|
|
||||||
public getAgendaSubtitle(viewModel: V): string | null {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAgendaSlideTitle(titleInformation: T): string {
|
public getAgendaSlideTitle(titleInformation: T): string {
|
||||||
@ -68,19 +66,8 @@ export abstract class BaseIsAgendaItemAndListOfSpeakersContentObjectRepository<
|
|||||||
return numberPrefix + this.getTitle(titleInformation);
|
return numberPrefix + this.getTitle(titleInformation);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to get the list-title without the item-number.
|
|
||||||
*
|
|
||||||
* @param titleInformation The title-information for an object.
|
|
||||||
*
|
|
||||||
* @returns {string} The title without any prefix like item-number.
|
|
||||||
*/
|
|
||||||
public getAgendaListTitleWithoutItemNumber(titleInformation: T): string {
|
|
||||||
return this.getTitle(titleInformation) + ' (' + this.getVerboseName() + ')';
|
|
||||||
}
|
|
||||||
|
|
||||||
public getListOfSpeakersTitle = (titleInformation: T) => {
|
public getListOfSpeakersTitle = (titleInformation: T) => {
|
||||||
return this.getAgendaListTitle(titleInformation);
|
return this.getAgendaListTitle(titleInformation).title;
|
||||||
};
|
};
|
||||||
|
|
||||||
public getListOfSpeakersSlideTitle = (titleInformation: T) => {
|
public getListOfSpeakersSlideTitle = (titleInformation: T) => {
|
||||||
@ -90,9 +77,7 @@ export abstract class BaseIsAgendaItemAndListOfSpeakersContentObjectRepository<
|
|||||||
protected createViewModelWithTitles(model: M): V {
|
protected createViewModelWithTitles(model: M): V {
|
||||||
const viewModel = super.createViewModelWithTitles(model);
|
const viewModel = super.createViewModelWithTitles(model);
|
||||||
viewModel.getAgendaListTitle = () => this.getAgendaListTitle(viewModel);
|
viewModel.getAgendaListTitle = () => this.getAgendaListTitle(viewModel);
|
||||||
viewModel.getAgendaListTitleWithoutItemNumber = () => this.getAgendaListTitleWithoutItemNumber(viewModel);
|
|
||||||
viewModel.getAgendaSlideTitle = () => this.getAgendaSlideTitle(viewModel);
|
viewModel.getAgendaSlideTitle = () => this.getAgendaSlideTitle(viewModel);
|
||||||
viewModel.getAgendaSubtitle = () => this.getAgendaSubtitle(viewModel);
|
|
||||||
viewModel.getListOfSpeakersTitle = () => this.getListOfSpeakersTitle(viewModel);
|
viewModel.getListOfSpeakersTitle = () => this.getListOfSpeakersTitle(viewModel);
|
||||||
viewModel.getListOfSpeakersSlideTitle = () => this.getListOfSpeakersSlideTitle(viewModel);
|
viewModel.getListOfSpeakersSlideTitle = () => this.getListOfSpeakersSlideTitle(viewModel);
|
||||||
return viewModel;
|
return viewModel;
|
||||||
|
@ -2,6 +2,7 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
|
|
||||||
import { ViewItem } from 'app/site/agenda/models/view-item';
|
import { ViewItem } from 'app/site/agenda/models/view-item';
|
||||||
import {
|
import {
|
||||||
|
AgendaListTitle,
|
||||||
BaseViewModelWithAgendaItem,
|
BaseViewModelWithAgendaItem,
|
||||||
TitleInformationWithAgendaItem
|
TitleInformationWithAgendaItem
|
||||||
} from 'app/site/base/base-view-model-with-agenda-item';
|
} from 'app/site/base/base-view-model-with-agenda-item';
|
||||||
@ -29,8 +30,7 @@ export interface IBaseIsAgendaItemContentObjectRepository<
|
|||||||
M extends BaseModel,
|
M extends BaseModel,
|
||||||
T extends TitleInformationWithAgendaItem
|
T extends TitleInformationWithAgendaItem
|
||||||
> extends BaseRepository<V, M, T> {
|
> extends BaseRepository<V, M, T> {
|
||||||
getAgendaListTitle: (titleInformation: T) => string;
|
getAgendaListTitle: (titleInformation: T) => AgendaListTitle;
|
||||||
getAgendaListTitleWithoutItemNumber: (titleInformation: T) => string;
|
|
||||||
getAgendaSlideTitle: (titleInformation: T) => string;
|
getAgendaSlideTitle: (titleInformation: T) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,31 +77,11 @@ export abstract class BaseIsAgendaItemContentObjectRepository<
|
|||||||
* @returns the agenda title for the agenda item list. Should
|
* @returns the agenda title for the agenda item list. Should
|
||||||
* be `<item number> · <title> (<type>)`. E.g. `7 · the is an election (Election)`.
|
* be `<item number> · <title> (<type>)`. E.g. `7 · the is an election (Election)`.
|
||||||
*/
|
*/
|
||||||
public getAgendaListTitle(titleInformation: T): string {
|
public getAgendaListTitle(titleInformation: T): AgendaListTitle {
|
||||||
// Return the agenda title with the model's verbose name appended
|
// Return the agenda title with the model's verbose name appended
|
||||||
const numberPrefix = titleInformation.agenda_item_number() ? `${titleInformation.agenda_item_number()} · ` : '';
|
const numberPrefix = titleInformation.agenda_item_number() ? `${titleInformation.agenda_item_number()} · ` : '';
|
||||||
return numberPrefix + this.getTitle(titleInformation) + ' (' + this.getVerboseName() + ')';
|
const title = numberPrefix + this.getTitle(titleInformation) + ' (' + this.getVerboseName() + ')';
|
||||||
}
|
return { title };
|
||||||
|
|
||||||
/**
|
|
||||||
* Overrides the base function. Returns an optional subtitle.
|
|
||||||
*
|
|
||||||
* @param viewModel The model to get the subtitle from.
|
|
||||||
* @returns A string as subtitle. Defaults to `null`.
|
|
||||||
*/
|
|
||||||
public getAgendaSubtitle(viewModel: V): string | null {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to return the title without item-number, in example used for pdf-creation.
|
|
||||||
*
|
|
||||||
* @param titleInformation The title information.
|
|
||||||
*
|
|
||||||
* @returns {string} The title without any prefix like the item-number.
|
|
||||||
*/
|
|
||||||
public getAgendaListTitleWithoutItemNumber(titleInformation: T): string {
|
|
||||||
return this.getTitle(titleInformation) + ' (' + this.getVerboseName() + ')';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -117,9 +97,7 @@ export abstract class BaseIsAgendaItemContentObjectRepository<
|
|||||||
protected createViewModelWithTitles(model: M): V {
|
protected createViewModelWithTitles(model: M): V {
|
||||||
const viewModel = super.createViewModelWithTitles(model);
|
const viewModel = super.createViewModelWithTitles(model);
|
||||||
viewModel.getAgendaListTitle = () => this.getAgendaListTitle(viewModel);
|
viewModel.getAgendaListTitle = () => this.getAgendaListTitle(viewModel);
|
||||||
viewModel.getAgendaListTitleWithoutItemNumber = () => this.getAgendaListTitleWithoutItemNumber(viewModel);
|
|
||||||
viewModel.getAgendaSlideTitle = () => this.getAgendaSlideTitle(viewModel);
|
viewModel.getAgendaSlideTitle = () => this.getAgendaSlideTitle(viewModel);
|
||||||
viewModel.getAgendaSubtitle = () => this.getAgendaSubtitle(viewModel);
|
|
||||||
return viewModel;
|
return viewModel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import { BaseViewModel, TitleInformation, ViewModelConstructor } from '../../sit
|
|||||||
import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service';
|
import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service';
|
||||||
import { DataSendService } from '../core-services/data-send.service';
|
import { DataSendService } from '../core-services/data-send.service';
|
||||||
import { DataStoreService } from '../core-services/data-store.service';
|
import { DataStoreService } from '../core-services/data-store.service';
|
||||||
|
import { HasViewModelListObservable } from '../definitions/has-view-model-list-observable';
|
||||||
import { Identifiable } from '../../shared/models/base/identifiable';
|
import { Identifiable } from '../../shared/models/base/identifiable';
|
||||||
import { OnAfterAppsLoaded } from '../definitions/on-after-apps-loaded';
|
import { OnAfterAppsLoaded } from '../definitions/on-after-apps-loaded';
|
||||||
import { RelationManagerService } from '../core-services/relation-manager.service';
|
import { RelationManagerService } from '../core-services/relation-manager.service';
|
||||||
@ -30,7 +31,7 @@ export interface NestedModelDescriptors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export abstract class BaseRepository<V extends BaseViewModel & T, M extends BaseModel, T extends TitleInformation>
|
export abstract class BaseRepository<V extends BaseViewModel & T, M extends BaseModel, T extends TitleInformation>
|
||||||
implements OnAfterAppsLoaded, Collection {
|
implements OnAfterAppsLoaded, Collection, HasViewModelListObservable<V> {
|
||||||
/**
|
/**
|
||||||
* Stores all the viewModel in an object
|
* Stores all the viewModel in an object
|
||||||
*/
|
*/
|
||||||
@ -42,8 +43,8 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
|||||||
protected viewModelSubjects: { [modelId: number]: BehaviorSubject<V> } = {};
|
protected viewModelSubjects: { [modelId: number]: BehaviorSubject<V> } = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable subject for the whole list. These entries are unsorted an not piped through
|
* Observable subject for the whole list. These entries are unsorted and not piped through
|
||||||
* autodTime. Just use this internally.
|
* auditTime. Just use this internally.
|
||||||
*
|
*
|
||||||
* It's used to debounce messages on the sortedViewModelListSubject
|
* It's used to debounce messages on the sortedViewModelListSubject
|
||||||
*/
|
*/
|
||||||
@ -188,7 +189,7 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* After creating a view model, all functions for models form the repo
|
* After creating a view model, all functions for models from the repo
|
||||||
* are assigned to the new view model.
|
* are assigned to the new view model.
|
||||||
*/
|
*/
|
||||||
protected createViewModelWithTitles(model: M): V {
|
protected createViewModelWithTitles(model: M): V {
|
||||||
@ -269,7 +270,7 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
|||||||
this.viewModelStore = {};
|
this.viewModelStore = {};
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* The function used for sorting the data of this repository. The defualt sorts by ID.
|
* The function used for sorting the data of this repository. The default sorts by ID.
|
||||||
*/
|
*/
|
||||||
protected viewModelSortFn: (a: V, b: V) => number = (a: V, b: V) => a.id - b.id;
|
protected viewModelSortFn: (a: V, b: V) => number = (a: V, b: V) => a.id - b.id;
|
||||||
|
|
||||||
|
@ -80,7 +80,8 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for ConfigRepositoryService. Requests the constants from the server and creates the config group structure.
|
* Constructor for ConfigRepositoryService. Requests the constants from the server and creates the config
|
||||||
|
* group structure.
|
||||||
*
|
*
|
||||||
* @param DS The DataStore
|
* @param DS The DataStore
|
||||||
* @param mapperService Maps collection strings to classes
|
* @param mapperService Maps collection strings to classes
|
||||||
|
@ -8,7 +8,7 @@ describe('FileRepositoryService', () => {
|
|||||||
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
const service: MediafileRepositoryService = TestBed.get(MediafileRepositoryService);
|
const service: MediafileRepositoryService = TestBed.inject(MediafileRepositoryService);
|
||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
import { ChangeRecoMode } from 'app/site/motions/motions.constants';
|
import { ChangeRecoMode } from 'app/site/motions/motions.constants';
|
||||||
import { BaseRepository } from '../base-repository';
|
import { BaseRepository } from '../base-repository';
|
||||||
import { DiffService, LineRange, ModificationType } from '../../ui-services/diff.service';
|
import { DiffService, LineRange, ModificationType } from '../../ui-services/diff.service';
|
||||||
|
import { LinenumberingService } from '../../ui-services/linenumbering.service';
|
||||||
import { ViewMotion } from '../../../site/motions/models/view-motion';
|
import { ViewMotion } from '../../../site/motions/models/view-motion';
|
||||||
import { ViewUnifiedChange } from '../../../shared/models/motions/view-unified-change';
|
import { ViewUnifiedChange } from '../../../shared/models/motions/view-unified-change';
|
||||||
|
|
||||||
@ -50,7 +51,9 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
|
|||||||
* @param {CollectionStringMapperService} mapperService Maps collection strings to classes
|
* @param {CollectionStringMapperService} mapperService Maps collection strings to classes
|
||||||
* @param {ViewModelStoreService} viewModelStoreService
|
* @param {ViewModelStoreService} viewModelStoreService
|
||||||
* @param {TranslateService} translate
|
* @param {TranslateService} translate
|
||||||
|
* @param {RelationManagerService} relationManager
|
||||||
* @param {DiffService} diffService
|
* @param {DiffService} diffService
|
||||||
|
* @param {LinenumberingService} lineNumbering Line numbering service
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
DS: DataStoreService,
|
DS: DataStoreService,
|
||||||
@ -59,7 +62,8 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
|
|||||||
viewModelStoreService: ViewModelStoreService,
|
viewModelStoreService: ViewModelStoreService,
|
||||||
translate: TranslateService,
|
translate: TranslateService,
|
||||||
relationManager: RelationManagerService,
|
relationManager: RelationManagerService,
|
||||||
private diffService: DiffService
|
private diffService: DiffService,
|
||||||
|
private lineNumbering: LinenumberingService
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
DS,
|
DS,
|
||||||
@ -103,7 +107,7 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
|
|||||||
/**
|
/**
|
||||||
* Synchronously getting the change recommendations of the corresponding motion.
|
* Synchronously getting the change recommendations of the corresponding motion.
|
||||||
*
|
*
|
||||||
* @param motionId the id of the target motion
|
* @param motion_id the id of the target motion
|
||||||
* @returns the array of change recommendations to the motions.
|
* @returns the array of change recommendations to the motions.
|
||||||
*/
|
*/
|
||||||
public getChangeRecoOfMotion(motion_id: number): ViewMotionChangeRecommendation[] {
|
public getChangeRecoOfMotion(motion_id: number): ViewMotionChangeRecommendation[] {
|
||||||
@ -171,22 +175,61 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
|
|||||||
* @param {LineRange} lineRange
|
* @param {LineRange} lineRange
|
||||||
* @param {number} lineLength
|
* @param {number} lineLength
|
||||||
*/
|
*/
|
||||||
public createChangeRecommendationTemplate(
|
public createMotionChangeRecommendationTemplate(
|
||||||
motion: ViewMotion,
|
motion: ViewMotion,
|
||||||
lineRange: LineRange,
|
lineRange: LineRange,
|
||||||
lineLength: number
|
lineLength: number
|
||||||
): ViewMotionChangeRecommendation {
|
): ViewMotionChangeRecommendation {
|
||||||
|
const motionText = this.lineNumbering.insertLineNumbers(motion.text, lineLength);
|
||||||
|
|
||||||
const changeReco = new MotionChangeRecommendation();
|
const changeReco = new MotionChangeRecommendation();
|
||||||
changeReco.line_from = lineRange.from;
|
changeReco.line_from = lineRange.from;
|
||||||
changeReco.line_to = lineRange.to;
|
changeReco.line_to = lineRange.to;
|
||||||
changeReco.type = ModificationType.TYPE_REPLACEMENT;
|
changeReco.type = ModificationType.TYPE_REPLACEMENT;
|
||||||
changeReco.text = this.diffService.extractMotionLineRange(motion.text, lineRange, false, lineLength, null);
|
changeReco.text = this.diffService.extractMotionLineRange(motionText, lineRange, false, lineLength, null);
|
||||||
changeReco.rejected = false;
|
changeReco.rejected = false;
|
||||||
changeReco.motion_id = motion.id;
|
changeReco.motion_id = motion.id;
|
||||||
|
|
||||||
return new ViewMotionChangeRecommendation(changeReco);
|
return new ViewMotionChangeRecommendation(changeReco);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link ViewMotionChangeRecommendation} object based on the amendment ID, the precalculated
|
||||||
|
* paragraphs (because we don't have access to motion-repository serice here) and the given lange range.
|
||||||
|
* This object is not saved yet and does not yet have any changed HTML. It's meant to populate the UI form.
|
||||||
|
*
|
||||||
|
* @param {ViewMotion} amendment
|
||||||
|
* @param {string[]} lineNumberedParagraphs
|
||||||
|
* @param {LineRange} lineRange
|
||||||
|
* @param {number} lineLength
|
||||||
|
*/
|
||||||
|
public createAmendmentChangeRecommendationTemplate(
|
||||||
|
amendment: ViewMotion,
|
||||||
|
lineNumberedParagraphs: string[],
|
||||||
|
lineRange: LineRange,
|
||||||
|
lineLength: number
|
||||||
|
): ViewMotionChangeRecommendation {
|
||||||
|
const consolidatedText = lineNumberedParagraphs.join('\n');
|
||||||
|
|
||||||
|
const extracted = this.diffService.extractRangeByLineNumbers(consolidatedText, lineRange.from, lineRange.to);
|
||||||
|
const extractedHtml =
|
||||||
|
extracted.outerContextStart +
|
||||||
|
extracted.innerContextStart +
|
||||||
|
extracted.html +
|
||||||
|
extracted.innerContextEnd +
|
||||||
|
extracted.outerContextEnd;
|
||||||
|
|
||||||
|
const changeReco = new MotionChangeRecommendation();
|
||||||
|
changeReco.line_from = lineRange.from;
|
||||||
|
changeReco.line_to = lineRange.to;
|
||||||
|
changeReco.type = ModificationType.TYPE_REPLACEMENT;
|
||||||
|
changeReco.rejected = false;
|
||||||
|
changeReco.motion_id = amendment.id;
|
||||||
|
changeReco.text = extractedHtml;
|
||||||
|
|
||||||
|
return new ViewMotionChangeRecommendation(changeReco);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a {@link ViewMotionChangeRecommendation} object to change the title, based on the motion ID.
|
* Creates a {@link ViewMotionChangeRecommendation} object to change the title, based on the motion ID.
|
||||||
* This object is not saved yet and does not yet have any changed title. It's meant to populate the UI form.
|
* This object is not saved yet and does not yet have any changed title. It's meant to populate the UI form.
|
||||||
|
@ -12,7 +12,7 @@ describe('MotionBlockRepositoryService', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
const service: MotionBlockRepositoryService = TestBed.get(MotionBlockRepositoryService);
|
const service: MotionBlockRepositoryService = TestBed.inject(MotionBlockRepositoryService);
|
||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { MotionOptionRepositoryService } from './motion-option-repository.service';
|
||||||
|
|
||||||
|
describe('MotionOptionRepositoryService', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: MotionOptionRepositoryService = TestBed.inject(MotionOptionRepositoryService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,68 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { DataSendService } from 'app/core/core-services/data-send.service';
|
||||||
|
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
||||||
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
|
import { RelationDefinition } from 'app/core/definitions/relations';
|
||||||
|
import { MotionOption } from 'app/shared/models/motions/motion-option';
|
||||||
|
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
||||||
|
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||||
|
import { ViewMotionVote } from 'app/site/motions/models/view-motion-vote';
|
||||||
|
import { BaseRepository } from '../base-repository';
|
||||||
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
|
|
||||||
|
const MotionOptionRelations: RelationDefinition[] = [
|
||||||
|
{
|
||||||
|
type: 'O2M',
|
||||||
|
foreignIdKey: 'option_id',
|
||||||
|
ownKey: 'votes',
|
||||||
|
foreignViewModel: ViewMotionVote
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'M2O',
|
||||||
|
ownIdKey: 'poll_id',
|
||||||
|
ownKey: 'poll',
|
||||||
|
foreignViewModel: ViewMotionPoll
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository Service for Options.
|
||||||
|
*
|
||||||
|
* Documentation partially provided in {@link BaseRepository}
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class MotionOptionRepositoryService extends BaseRepository<ViewMotionOption, MotionOption, object> {
|
||||||
|
public constructor(
|
||||||
|
DS: DataStoreService,
|
||||||
|
dataSend: DataSendService,
|
||||||
|
mapperService: CollectionStringMapperService,
|
||||||
|
viewModelStoreService: ViewModelStoreService,
|
||||||
|
translate: TranslateService,
|
||||||
|
relationManager: RelationManagerService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
DS,
|
||||||
|
dataSend,
|
||||||
|
mapperService,
|
||||||
|
viewModelStoreService,
|
||||||
|
translate,
|
||||||
|
relationManager,
|
||||||
|
MotionOption,
|
||||||
|
MotionOptionRelations
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTitle = (titleInformation: object) => {
|
||||||
|
return 'Option';
|
||||||
|
};
|
||||||
|
|
||||||
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
|
return this.translate.instant(plural ? 'Options' : 'Option');
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { MotionPollRepositoryService } from './motion-poll-repository.service';
|
||||||
|
|
||||||
|
describe('MotionPollRepositoryService', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: MotionPollRepositoryService = TestBed.inject(MotionPollRepositoryService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,98 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { DataSendService } from 'app/core/core-services/data-send.service';
|
||||||
|
import { HttpService } from 'app/core/core-services/http.service';
|
||||||
|
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
||||||
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
|
import { RelationDefinition } from 'app/core/definitions/relations';
|
||||||
|
import { VotingService } from 'app/core/ui-services/voting.service';
|
||||||
|
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
|
||||||
|
import { VoteValue } from 'app/shared/models/poll/base-vote';
|
||||||
|
import { ViewMotion } from 'app/site/motions/models/view-motion';
|
||||||
|
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
||||||
|
import { MotionPollTitleInformation, ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||||
|
import { BasePollRepositoryService } from 'app/site/polls/services/base-poll-repository.service';
|
||||||
|
import { ViewGroup } from 'app/site/users/models/view-group';
|
||||||
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
|
|
||||||
|
const MotionPollRelations: RelationDefinition[] = [
|
||||||
|
{
|
||||||
|
type: 'M2M',
|
||||||
|
ownIdKey: 'groups_id',
|
||||||
|
ownKey: 'groups',
|
||||||
|
foreignViewModel: ViewGroup
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'O2M',
|
||||||
|
ownIdKey: 'options_id',
|
||||||
|
ownKey: 'options',
|
||||||
|
foreignViewModel: ViewMotionOption
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'M2O',
|
||||||
|
ownIdKey: 'motion_id',
|
||||||
|
ownKey: 'motion',
|
||||||
|
foreignViewModel: ViewMotion
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'M2M',
|
||||||
|
ownIdKey: 'voted_id',
|
||||||
|
ownKey: 'voted',
|
||||||
|
foreignViewModel: ViewUser
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository Service for Assignments.
|
||||||
|
*
|
||||||
|
* Documentation partially provided in {@link BaseRepository}
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class MotionPollRepositoryService extends BasePollRepositoryService<
|
||||||
|
ViewMotionPoll,
|
||||||
|
MotionPoll,
|
||||||
|
MotionPollTitleInformation
|
||||||
|
> {
|
||||||
|
public constructor(
|
||||||
|
DS: DataStoreService,
|
||||||
|
dataSend: DataSendService,
|
||||||
|
mapperService: CollectionStringMapperService,
|
||||||
|
viewModelStoreService: ViewModelStoreService,
|
||||||
|
translate: TranslateService,
|
||||||
|
relationManager: RelationManagerService,
|
||||||
|
votingService: VotingService,
|
||||||
|
http: HttpService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
DS,
|
||||||
|
dataSend,
|
||||||
|
mapperService,
|
||||||
|
viewModelStoreService,
|
||||||
|
translate,
|
||||||
|
relationManager,
|
||||||
|
MotionPoll,
|
||||||
|
MotionPollRelations,
|
||||||
|
{},
|
||||||
|
votingService,
|
||||||
|
http
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTitle = (titleInformation: MotionPollTitleInformation) => {
|
||||||
|
return titleInformation.title;
|
||||||
|
};
|
||||||
|
|
||||||
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
|
return this.translate.instant(plural ? 'Polls' : 'Poll');
|
||||||
|
};
|
||||||
|
|
||||||
|
public vote(vote: VoteValue, poll_id: number): Promise<void> {
|
||||||
|
return this.http.post(`/rest/motions/motion-poll/${poll_id}/vote/`, JSON.stringify(vote));
|
||||||
|
}
|
||||||
|
}
|
@ -14,16 +14,17 @@ import { ConfigService } from 'app/core/ui-services/config.service';
|
|||||||
import { DiffLinesInParagraph, DiffService } from 'app/core/ui-services/diff.service';
|
import { DiffLinesInParagraph, DiffService } from 'app/core/ui-services/diff.service';
|
||||||
import { TreeIdNode } from 'app/core/ui-services/tree.service';
|
import { TreeIdNode } from 'app/core/ui-services/tree.service';
|
||||||
import { Motion } from 'app/shared/models/motions/motion';
|
import { Motion } from 'app/shared/models/motions/motion';
|
||||||
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
|
|
||||||
import { Submitter } from 'app/shared/models/motions/submitter';
|
import { Submitter } from 'app/shared/models/motions/submitter';
|
||||||
import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change';
|
import { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change';
|
||||||
import { PersonalNoteContent } from 'app/shared/models/users/personal-note';
|
import { PersonalNoteContent } from 'app/shared/models/users/personal-note';
|
||||||
|
import { AgendaListTitle } from 'app/site/base/base-view-model-with-agenda-item';
|
||||||
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
||||||
import { ViewCategory } from 'app/site/motions/models/view-category';
|
import { ViewCategory } from 'app/site/motions/models/view-category';
|
||||||
import { MotionTitleInformation, ViewMotion } from 'app/site/motions/models/view-motion';
|
import { MotionTitleInformation, ViewMotion } from 'app/site/motions/models/view-motion';
|
||||||
import { ViewMotionAmendedParagraph } from 'app/site/motions/models/view-motion-amended-paragraph';
|
import { ViewMotionAmendedParagraph } from 'app/site/motions/models/view-motion-amended-paragraph';
|
||||||
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
|
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
|
||||||
import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation';
|
import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation';
|
||||||
|
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||||
import { ViewState } from 'app/site/motions/models/view-state';
|
import { ViewState } from 'app/site/motions/models/view-state';
|
||||||
import { ViewStatuteParagraph } from 'app/site/motions/models/view-statute-paragraph';
|
import { ViewStatuteParagraph } from 'app/site/motions/models/view-statute-paragraph';
|
||||||
import { ViewSubmitter } from 'app/site/motions/models/view-submitter';
|
import { ViewSubmitter } from 'app/site/motions/models/view-submitter';
|
||||||
@ -36,7 +37,7 @@ import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../bas
|
|||||||
import { NestedModelDescriptors } from '../base-repository';
|
import { NestedModelDescriptors } from '../base-repository';
|
||||||
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
import { DataSendService } from '../../core-services/data-send.service';
|
import { DataSendService } from '../../core-services/data-send.service';
|
||||||
import { LinenumberingService, LineNumberRange } from '../../ui-services/linenumbering.service';
|
import { LineNumberedString, LinenumberingService, LineNumberRange } from '../../ui-services/linenumbering.service';
|
||||||
|
|
||||||
type SortProperty = 'weight' | 'identifier';
|
type SortProperty = 'weight' | 'identifier';
|
||||||
|
|
||||||
@ -126,12 +127,17 @@ const MotionRelations: RelationDefinition[] = [
|
|||||||
ownKey: 'amendments',
|
ownKey: 'amendments',
|
||||||
foreignViewModel: ViewMotion
|
foreignViewModel: ViewMotion
|
||||||
},
|
},
|
||||||
// TMP:
|
|
||||||
{
|
{
|
||||||
type: 'M2O',
|
type: 'M2O',
|
||||||
ownIdKey: 'parent_id',
|
ownIdKey: 'parent_id',
|
||||||
ownKey: 'parent',
|
ownKey: 'parent',
|
||||||
foreignViewModel: ViewMotion
|
foreignViewModel: ViewMotion
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'O2M',
|
||||||
|
foreignIdKey: 'motion_id',
|
||||||
|
ownKey: 'polls',
|
||||||
|
foreignViewModel: ViewMotionPoll
|
||||||
}
|
}
|
||||||
// Personal notes are dynamically added in the repo.
|
// Personal notes are dynamically added in the repo.
|
||||||
];
|
];
|
||||||
@ -195,11 +201,14 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
|||||||
* @param DS The DataStore
|
* @param DS The DataStore
|
||||||
* @param mapperService Maps collection strings to classes
|
* @param mapperService Maps collection strings to classes
|
||||||
* @param dataSend sending changed objects
|
* @param dataSend sending changed objects
|
||||||
|
* @param viewModelStoreService ViewModelStoreService
|
||||||
|
* @param translate
|
||||||
|
* @param relationManager
|
||||||
* @param httpService OpenSlides own Http service
|
* @param httpService OpenSlides own Http service
|
||||||
* @param lineNumbering Line numbering for motion text
|
* @param lineNumbering Line numbering for motion text
|
||||||
* @param diff Display changes in motion text as diff.
|
* @param diff Display changes in motion text as diff.
|
||||||
* @param personalNoteService service fo personal notes
|
|
||||||
* @param config ConfigService (subscribe to sorting config)
|
* @param config ConfigService (subscribe to sorting config)
|
||||||
|
* @param operator
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
DS: DataStoreService,
|
DS: DataStoreService,
|
||||||
@ -264,46 +273,40 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
|||||||
public getAgendaListTitle = (titleInformation: MotionTitleInformation) => {
|
public getAgendaListTitle = (titleInformation: MotionTitleInformation) => {
|
||||||
const numberPrefix = titleInformation.agenda_item_number() ? `${titleInformation.agenda_item_number()} · ` : '';
|
const numberPrefix = titleInformation.agenda_item_number() ? `${titleInformation.agenda_item_number()} · ` : '';
|
||||||
// Append the verbose name only, if not the special format 'Motion <identifier>' is used.
|
// Append the verbose name only, if not the special format 'Motion <identifier>' is used.
|
||||||
|
let title;
|
||||||
if (titleInformation.identifier) {
|
if (titleInformation.identifier) {
|
||||||
return `${numberPrefix}${this.translate.instant('Motion')} ${titleInformation.identifier} · ${
|
title = `${numberPrefix}${this.translate.instant('Motion')} ${titleInformation.identifier} · ${
|
||||||
titleInformation.title
|
titleInformation.title
|
||||||
}`;
|
}`;
|
||||||
} else {
|
} else {
|
||||||
return `${numberPrefix}${titleInformation.title} (${this.getVerboseName()})`;
|
title = `${numberPrefix}${titleInformation.title} (${this.getVerboseName()})`;
|
||||||
}
|
}
|
||||||
};
|
const agendaTitle: AgendaListTitle = { title };
|
||||||
|
|
||||||
/**
|
// Subtitle.
|
||||||
* @override The base function and returns the submitters as optional subtitle.
|
// This is a bit hacky: If one has not motions.can_see, the titleinformation is nut sufficient for
|
||||||
*/
|
// submitters. So try-cast titleInformation to a ViewMotion and check, if submittersAsUsers is available
|
||||||
public getAgendaSubtitle = (motion: ViewMotion) => {
|
const viewMotion: ViewMotion = titleInformation as ViewMotion;
|
||||||
if (motion.submittersAsUsers && motion.submittersAsUsers.length) {
|
if (viewMotion.submittersAsUsers && viewMotion.submittersAsUsers.length) {
|
||||||
return `${this.translate.instant('by')} ${motion.submittersAsUsers.join(', ')}`;
|
agendaTitle.subtitle = `${this.translate.instant('by')} ${viewMotion.submittersAsUsers.join(', ')}`;
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @override The base function
|
|
||||||
*/
|
|
||||||
public getAgendaListTitleWithoutItemNumber = (titleInformation: MotionTitleInformation) => {
|
|
||||||
if (titleInformation.identifier) {
|
|
||||||
return this.translate.instant('Motion') + ' ' + titleInformation.identifier;
|
|
||||||
} else {
|
|
||||||
return titleInformation.title + `(${this.getVerboseName()})`;
|
|
||||||
}
|
}
|
||||||
|
return agendaTitle;
|
||||||
};
|
};
|
||||||
|
|
||||||
public getVerboseName = (plural: boolean = false) => {
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
return this.translate.instant(plural ? 'Motions' : 'Motion');
|
return this.translate.instant(plural ? 'Motions' : 'Motion');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public getProjectorTitle = (viewMotion: ViewMotion) => {
|
||||||
|
const subtitle = viewMotion.item && viewMotion.item.comment ? viewMotion.item.comment : null;
|
||||||
|
return { title: this.getAgendaSlideTitle(viewMotion), subtitle };
|
||||||
|
};
|
||||||
|
|
||||||
protected createViewModelWithTitles(model: Motion): ViewMotion {
|
protected createViewModelWithTitles(model: Motion): ViewMotion {
|
||||||
const viewModel = super.createViewModelWithTitles(model);
|
const viewModel = super.createViewModelWithTitles(model);
|
||||||
|
|
||||||
viewModel.getIdentifierOrTitle = () => this.getIdentifierOrTitle(viewModel);
|
viewModel.getIdentifierOrTitle = () => this.getIdentifierOrTitle(viewModel);
|
||||||
viewModel.getProjectorTitle = () => this.getAgendaSlideTitle(viewModel);
|
viewModel.getProjectorTitle = () => this.getProjectorTitle(viewModel);
|
||||||
|
|
||||||
return viewModel;
|
return viewModel;
|
||||||
}
|
}
|
||||||
@ -321,8 +324,19 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
|||||||
type: 'custom',
|
type: 'custom',
|
||||||
ownKey: 'diffLines',
|
ownKey: 'diffLines',
|
||||||
get: (motion: Motion, viewMotion: ViewMotion) => {
|
get: (motion: Motion, viewMotion: ViewMotion) => {
|
||||||
if (viewMotion.parent) {
|
if (viewMotion.parent && viewMotion.isParagraphBasedAmendment()) {
|
||||||
return this.getAmendmentParagraphs(viewMotion, this.motionLineLength, false);
|
const changeRecos = viewMotion.changeRecommendations.filter(changeReco =>
|
||||||
|
changeReco.showInFinalView()
|
||||||
|
);
|
||||||
|
return this.getAmendmentParagraphLines(
|
||||||
|
viewMotion,
|
||||||
|
this.motionLineLength,
|
||||||
|
ChangeRecoMode.Changed,
|
||||||
|
changeRecos,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getCacheObjectToCheck: (viewMotion: ViewMotion) => viewMotion.parent
|
getCacheObjectToCheck: (viewMotion: ViewMotion) => viewMotion.parent
|
||||||
@ -376,7 +390,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
|||||||
/**
|
/**
|
||||||
* Set the state of motions in bulk
|
* Set the state of motions in bulk
|
||||||
*
|
*
|
||||||
* @param viewMotion target motion
|
* @param viewMotions target motions
|
||||||
* @param stateId the number that indicates the state
|
* @param stateId the number that indicates the state
|
||||||
*/
|
*/
|
||||||
public async setMultiState(viewMotions: ViewMotion[], stateId: number): Promise<void> {
|
public async setMultiState(viewMotions: ViewMotion[], stateId: number): Promise<void> {
|
||||||
@ -390,7 +404,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
|||||||
/**
|
/**
|
||||||
* Set the motion blocks of motions in bulk
|
* Set the motion blocks of motions in bulk
|
||||||
*
|
*
|
||||||
* @param viewMotion target motion
|
* @param viewMotions target motions
|
||||||
* @param motionblockId the number that indicates the motion block
|
* @param motionblockId the number that indicates the motion block
|
||||||
*/
|
*/
|
||||||
public async setMultiMotionBlock(viewMotions: ViewMotion[], motionblockId: number): Promise<void> {
|
public async setMultiMotionBlock(viewMotions: ViewMotion[], motionblockId: number): Promise<void> {
|
||||||
@ -404,7 +418,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
|||||||
/**
|
/**
|
||||||
* Set the category of motions in bulk
|
* Set the category of motions in bulk
|
||||||
*
|
*
|
||||||
* @param viewMotion target motion
|
* @param viewMotions target motions
|
||||||
* @param categoryId the number that indicates the category
|
* @param categoryId the number that indicates the category
|
||||||
*/
|
*/
|
||||||
public async setMultiCategory(viewMotions: ViewMotion[], categoryId: number): Promise<void> {
|
public async setMultiCategory(viewMotions: ViewMotion[], categoryId: number): Promise<void> {
|
||||||
@ -609,11 +623,12 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
|||||||
case ChangeRecoMode.Diff:
|
case ChangeRecoMode.Diff:
|
||||||
const text = [];
|
const text = [];
|
||||||
const changesToShow = changes.filter(change => change.showInDiffView());
|
const changesToShow = changes.filter(change => change.showInDiffView());
|
||||||
|
const motionText = this.lineNumbering.insertLineNumbers(targetMotion.text, lineLength);
|
||||||
|
|
||||||
for (let i = 0; i < changesToShow.length; i++) {
|
for (let i = 0; i < changesToShow.length; i++) {
|
||||||
text.push(
|
text.push(
|
||||||
this.diff.extractMotionLineRange(
|
this.diff.extractMotionLineRange(
|
||||||
targetMotion.text,
|
motionText,
|
||||||
{
|
{
|
||||||
from: i === 0 ? 1 : changesToShow[i - 1].getLineTo(),
|
from: i === 0 ? 1 : changesToShow[i - 1].getLineTo(),
|
||||||
to: changesToShow[i].getLineFrom()
|
to: changesToShow[i].getLineFrom()
|
||||||
@ -624,18 +639,11 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
text.push(
|
text.push(this.diff.getChangeDiff(motionText, changesToShow[i], lineLength, highlightLine));
|
||||||
this.diff.getChangeDiff(targetMotion.text, changesToShow[i], lineLength, highlightLine)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
text.push(
|
text.push(
|
||||||
this.diff.getTextRemainderAfterLastChange(
|
this.diff.getTextRemainderAfterLastChange(motionText, changesToShow, lineLength, highlightLine)
|
||||||
targetMotion.text,
|
|
||||||
changesToShow,
|
|
||||||
lineLength,
|
|
||||||
highlightLine
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
return text.join('');
|
return text.join('');
|
||||||
case ChangeRecoMode.Final:
|
case ChangeRecoMode.Final:
|
||||||
@ -715,79 +723,221 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
|||||||
* @param {number} lineLength
|
* @param {number} lineLength
|
||||||
*/
|
*/
|
||||||
public getParagraphsToChoose(motion: ViewMotion, lineLength: number): ParagraphToChoose[] {
|
public getParagraphsToChoose(motion: ViewMotion, lineLength: number): ParagraphToChoose[] {
|
||||||
return this.getTextParagraphs(motion, true, lineLength).map((paragraph: string, index: number) => {
|
const parent = motion.hasParent ? motion.parent : motion;
|
||||||
const affected: LineNumberRange = this.lineNumbering.getLineNumberRange(paragraph);
|
return this.getTextParagraphs(parent, true, lineLength).map((paragraph: string, index: number) => {
|
||||||
return {
|
let localParagraph;
|
||||||
paragraphNo: index,
|
if (motion.hasParent) {
|
||||||
html: this.lineNumbering.stripLineNumbers(paragraph),
|
localParagraph = motion.amendment_paragraphs[index] ? motion.amendment_paragraphs[index] : paragraph;
|
||||||
lineFrom: affected.from,
|
} else {
|
||||||
lineTo: affected.to
|
localParagraph = paragraph;
|
||||||
};
|
}
|
||||||
|
return this.extractAffectedParagraphs(localParagraph, index);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all paragraphs that are affected by the given amendment in diff-format
|
* To create paragraph based amendments for amendments, creates diffed paragraphs
|
||||||
|
* for selection
|
||||||
|
*/
|
||||||
|
public getDiffedParagraphToChoose(amendment: ViewMotion, lineLength: number): ParagraphToChoose[] {
|
||||||
|
if (amendment.hasParent) {
|
||||||
|
const parent = amendment.parent;
|
||||||
|
|
||||||
|
return this.getTextParagraphs(parent, true, lineLength).map((paragraph: string, index: number) => {
|
||||||
|
const diffedParagraph = amendment.amendment_paragraphs[index]
|
||||||
|
? this.diff.diff(paragraph, amendment.amendment_paragraphs[index], lineLength)
|
||||||
|
: paragraph;
|
||||||
|
return this.extractAffectedParagraphs(diffedParagraph, index);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error('getDiffedParagraphToChoose: given amendment has no parent');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a selectable and editable paragraph
|
||||||
|
*/
|
||||||
|
private extractAffectedParagraphs(paragraph: string, index: number): ParagraphToChoose {
|
||||||
|
const affected: LineNumberRange = this.lineNumbering.getLineNumberRange(paragraph);
|
||||||
|
return {
|
||||||
|
paragraphNo: index,
|
||||||
|
html: this.lineNumbering.stripLineNumbers(paragraph),
|
||||||
|
lineFrom: affected.from,
|
||||||
|
lineTo: affected.to
|
||||||
|
} as ParagraphToChoose;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the amended paragraphs by an amendment. Correlates to the amendment_paragraphs field,
|
||||||
|
* but also considers relevant change recommendations.
|
||||||
|
* The returned array includes "null" values for paragraphs that have not been changed.
|
||||||
*
|
*
|
||||||
* @param {ViewMotion} amendment
|
* @param {ViewMotion} amendment
|
||||||
* @param {number} lineLength
|
* @param {number} lineLength
|
||||||
|
* @param {ViewMotionChangeRecommendation[]} changes
|
||||||
|
* @param {boolean} includeUnchanged
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
public applyChangesToAmendment(
|
||||||
|
amendment: ViewMotion,
|
||||||
|
lineLength: number,
|
||||||
|
changes: ViewMotionChangeRecommendation[],
|
||||||
|
includeUnchanged: boolean
|
||||||
|
): string[] {
|
||||||
|
const motion = amendment.parent;
|
||||||
|
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
|
||||||
|
|
||||||
|
// Changes need to be applied from the bottom up, to prevent conflicts with changing line numbers.
|
||||||
|
changes.sort((change1: ViewUnifiedChange, change2: ViewUnifiedChange) => {
|
||||||
|
if (change1.getLineFrom() < change2.getLineFrom()) {
|
||||||
|
return 1;
|
||||||
|
} else if (change1.getLineFrom() > change2.getLineFrom()) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return amendment.amendment_paragraphs?.map((newText: string, paraNo: number) => {
|
||||||
|
let paragraph: string;
|
||||||
|
let paragraphHasChanges;
|
||||||
|
|
||||||
|
if (newText === null) {
|
||||||
|
paragraph = baseParagraphs[paraNo];
|
||||||
|
paragraphHasChanges = false;
|
||||||
|
} else {
|
||||||
|
// Add line numbers to newText, relative to the baseParagraph, by creating a diff
|
||||||
|
// to the line numbered base version any applying it right away
|
||||||
|
const diff = this.diff.diff(baseParagraphs[paraNo], newText);
|
||||||
|
paragraph = this.diff.diffHtmlToFinalText(diff);
|
||||||
|
paragraphHasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const affected: LineNumberRange = this.lineNumbering.getLineNumberRange(paragraph);
|
||||||
|
|
||||||
|
changes.forEach((change: ViewMotionChangeRecommendation) => {
|
||||||
|
// Hint: this assumes that change recommendations only affect one specific paragraph, not multiple
|
||||||
|
if (change.line_from >= affected.from && change.line_from < affected.to) {
|
||||||
|
paragraph = this.diff.replaceLines(paragraph, change.text, change.line_from, change.line_to);
|
||||||
|
|
||||||
|
// Reapply relative line numbers
|
||||||
|
const diff = this.diff.diff(baseParagraphs[paraNo], paragraph);
|
||||||
|
paragraph = this.diff.diffHtmlToFinalText(diff);
|
||||||
|
|
||||||
|
paragraphHasChanges = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (paragraphHasChanges || includeUnchanged) {
|
||||||
|
return paragraph;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all paragraph lines that are affected by the given amendment in diff-format, including context.
|
||||||
|
*
|
||||||
|
* Should only be called for paragraph-based amendments.
|
||||||
|
*
|
||||||
|
* @param {ViewMotion} amendment
|
||||||
|
* @param {number} lineLength
|
||||||
|
* @param {ChangeRecoMode} crMode
|
||||||
|
* @param {ViewMotionChangeRecommendation[]} changeRecommendations
|
||||||
* @param {boolean} includeUnchanged
|
* @param {boolean} includeUnchanged
|
||||||
* @returns {DiffLinesInParagraph}
|
* @returns {DiffLinesInParagraph}
|
||||||
*/
|
*/
|
||||||
public getAmendmentParagraphs(
|
public getAmendmentParagraphLines(
|
||||||
amendment: ViewMotion,
|
amendment: ViewMotion,
|
||||||
lineLength: number,
|
lineLength: number,
|
||||||
|
crMode: ChangeRecoMode,
|
||||||
|
changeRecommendations: ViewMotionChangeRecommendation[],
|
||||||
includeUnchanged: boolean
|
includeUnchanged: boolean
|
||||||
): DiffLinesInParagraph[] {
|
): DiffLinesInParagraph[] {
|
||||||
const motion = amendment.parent;
|
const motion = amendment.parent;
|
||||||
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
|
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
|
||||||
|
|
||||||
return (amendment.amendment_paragraphs || [])
|
let amendmentParagraphs;
|
||||||
.map(
|
if (crMode === ChangeRecoMode.Changed) {
|
||||||
|
amendmentParagraphs = this.applyChangesToAmendment(amendment, lineLength, changeRecommendations, true);
|
||||||
|
} else {
|
||||||
|
amendmentParagraphs = amendment.amendment_paragraphs || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return amendmentParagraphs
|
||||||
|
?.map(
|
||||||
(newText: string, paraNo: number): DiffLinesInParagraph => {
|
(newText: string, paraNo: number): DiffLinesInParagraph => {
|
||||||
if (newText !== null) {
|
if (newText !== null) {
|
||||||
return this.diff.getAmendmentParagraphsLinesByMode(
|
return this.diff.getAmendmentParagraphsLines(
|
||||||
paraNo,
|
paraNo,
|
||||||
baseParagraphs[paraNo],
|
baseParagraphs[paraNo],
|
||||||
newText,
|
newText,
|
||||||
lineLength
|
lineLength
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Nothing has changed in this paragraph
|
return null; // Nothing has changed in this paragraph
|
||||||
if (includeUnchanged) {
|
|
||||||
const paragraph_line_range = this.lineNumbering.getLineNumberRange(baseParagraphs[paraNo]);
|
|
||||||
return {
|
|
||||||
paragraphNo: paraNo,
|
|
||||||
paragraphLineFrom: paragraph_line_range.from,
|
|
||||||
paragraphLineTo: paragraph_line_range.to,
|
|
||||||
diffLineFrom: paragraph_line_range.to,
|
|
||||||
diffLineTo: paragraph_line_range.to,
|
|
||||||
textPre: baseParagraphs[paraNo],
|
|
||||||
text: '',
|
|
||||||
textPost: ''
|
|
||||||
} as DiffLinesInParagraph;
|
|
||||||
} else {
|
|
||||||
return null; // null will make this paragraph filtered out
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
.map((diffLines: DiffLinesInParagraph, paraNo: number) => {
|
||||||
|
// If nothing has changed and we want to keep unchanged paragraphs for the context,
|
||||||
|
// return the original text in "textPre"
|
||||||
|
if (diffLines === null && includeUnchanged) {
|
||||||
|
const paragraph_line_range = this.lineNumbering.getLineNumberRange(baseParagraphs[paraNo]);
|
||||||
|
return {
|
||||||
|
paragraphNo: paraNo,
|
||||||
|
paragraphLineFrom: paragraph_line_range.from,
|
||||||
|
paragraphLineTo: paragraph_line_range.to,
|
||||||
|
diffLineFrom: paragraph_line_range.to,
|
||||||
|
diffLineTo: paragraph_line_range.to,
|
||||||
|
textPre: baseParagraphs[paraNo],
|
||||||
|
text: '',
|
||||||
|
textPost: ''
|
||||||
|
} as DiffLinesInParagraph;
|
||||||
|
} else {
|
||||||
|
return diffLines;
|
||||||
|
}
|
||||||
|
})
|
||||||
.filter((para: DiffLinesInParagraph) => para !== null);
|
.filter((para: DiffLinesInParagraph) => para !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAmendmentParagraphLinesTitle(paragraph: DiffLinesInParagraph): string {
|
||||||
|
if (paragraph.diffLineTo === paragraph.diffLineFrom + 1) {
|
||||||
|
return this.translate.instant('Line') + ' ' + paragraph.diffLineFrom.toString(10);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
this.translate.instant('Line') +
|
||||||
|
' ' +
|
||||||
|
paragraph.diffLineFrom.toString(10) +
|
||||||
|
' - ' +
|
||||||
|
(paragraph.diffLineTo - 1).toString(10)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all paragraphs that are affected by the given amendment as unified change objects.
|
* Returns all paragraphs that are affected by the given amendment as unified change objects.
|
||||||
|
* Only the affected part of each paragraph is returned.
|
||||||
|
* Change recommendations to this amendment are considered here, too. That is, if a change recommendation
|
||||||
|
* for an amendment exists and is not rejected, the changed amendment will be returned here.
|
||||||
*
|
*
|
||||||
* @param {ViewMotion} amendment
|
* @param {ViewMotion} amendment
|
||||||
* @param {number} lineLength
|
* @param {number} lineLength
|
||||||
|
* @param {ViewMotionChangeRecommendation[]} changeRecos
|
||||||
* @returns {ViewMotionAmendedParagraph[]}
|
* @returns {ViewMotionAmendedParagraph[]}
|
||||||
*/
|
*/
|
||||||
public getAmendmentAmendedParagraphs(amendment: ViewMotion, lineLength: number): ViewMotionAmendedParagraph[] {
|
public getAmendmentAmendedParagraphs(
|
||||||
|
amendment: ViewMotion,
|
||||||
|
lineLength: number,
|
||||||
|
changeRecos: ViewMotionChangeRecommendation[]
|
||||||
|
): ViewMotionAmendedParagraph[] {
|
||||||
const motion = amendment.parent;
|
const motion = amendment.parent;
|
||||||
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
|
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
|
||||||
|
const changedAmendmentParagraphs = this.applyChangesToAmendment(amendment, lineLength, changeRecos, false);
|
||||||
|
|
||||||
return (amendment.amendment_paragraphs || [])
|
return changedAmendmentParagraphs
|
||||||
.map(
|
?.map(
|
||||||
(newText: string, paraNo: number): ViewMotionAmendedParagraph => {
|
(newText: string, paraNo: number): ViewMotionAmendedParagraph => {
|
||||||
if (newText === null) {
|
if (newText === null) {
|
||||||
return null;
|
return null;
|
||||||
@ -812,43 +962,39 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a request to the server, creating a new poll for the motion
|
* For unchanged paragraphs, this returns the original motion paragraph, including line numbers.
|
||||||
*/
|
* For changed paragraphs, this returns the content of the amendment_paragraphs-field,
|
||||||
public async createPoll(motion: ViewMotion): Promise<void> {
|
* but including line numbers relative to the original motion line numbers,
|
||||||
const url = '/rest/motions/motion/' + motion.id + '/create_poll/';
|
* so they can be used for the amendment change recommendations
|
||||||
await this.httpService.post(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an update request for a poll.
|
|
||||||
*
|
*
|
||||||
* @param poll
|
* @param {ViewMotion} amendment
|
||||||
|
* @param {number} lineLength
|
||||||
|
* @param {boolean} withDiff
|
||||||
|
* @returns {LineNumberedString[]}
|
||||||
*/
|
*/
|
||||||
public async updatePoll(poll: MotionPoll): Promise<void> {
|
public getAllAmendmentParagraphsWithOriginalLineNumbers(
|
||||||
const url = '/rest/motions/motion-poll/' + poll.id + '/';
|
amendment: ViewMotion,
|
||||||
const data = {
|
lineLength: number,
|
||||||
motion_id: poll.motion_id,
|
withDiff: boolean
|
||||||
id: poll.id,
|
): LineNumberedString[] {
|
||||||
votescast: poll.votescast,
|
const motion = amendment.parent;
|
||||||
votesvalid: poll.votesvalid,
|
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
|
||||||
votesinvalid: poll.votesinvalid,
|
|
||||||
votes: {
|
return (amendment.amendment_paragraphs || []).map((newText: string, paraNo: number): string => {
|
||||||
Yes: poll.yes,
|
const origText = baseParagraphs[paraNo];
|
||||||
No: poll.no,
|
|
||||||
Abstain: poll.abstain
|
if (newText === null) {
|
||||||
|
return origText;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
await this.httpService.put(url, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const diff = this.diff.diff(origText, newText);
|
||||||
* Sends a http request to delete the given poll
|
|
||||||
*
|
if (withDiff) {
|
||||||
* @param poll
|
return diff;
|
||||||
*/
|
} else {
|
||||||
public async deletePoll(poll: MotionPoll): Promise<void> {
|
return this.diff.diffHtmlToFinalText(diff);
|
||||||
const url = '/rest/motions/motion-poll/' + poll.id + '/';
|
}
|
||||||
await this.httpService.delete(url);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { MotionVoteRepositoryService } from './motion-vote-repository.service';
|
||||||
|
|
||||||
|
describe('MotionVoteRepositoryService', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: MotionVoteRepositoryService = TestBed.inject(MotionVoteRepositoryService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,76 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { DataSendService } from 'app/core/core-services/data-send.service';
|
||||||
|
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
||||||
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
|
import { RelationDefinition } from 'app/core/definitions/relations';
|
||||||
|
import { MotionVote } from 'app/shared/models/motions/motion-vote';
|
||||||
|
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
|
||||||
|
import { ViewMotionVote } from 'app/site/motions/models/view-motion-vote';
|
||||||
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
|
import { BaseRepository } from '../base-repository';
|
||||||
|
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
|
||||||
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
|
|
||||||
|
const MotionVoteRelations: RelationDefinition[] = [
|
||||||
|
{
|
||||||
|
type: 'M2O',
|
||||||
|
ownIdKey: 'user_id',
|
||||||
|
ownKey: 'user',
|
||||||
|
foreignViewModel: ViewUser
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'M2O',
|
||||||
|
ownIdKey: 'option_id',
|
||||||
|
ownKey: 'option',
|
||||||
|
foreignViewModel: ViewMotionOption
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository Service for Assignments.
|
||||||
|
*
|
||||||
|
* Documentation partially provided in {@link BaseRepository}
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class MotionVoteRepositoryService extends BaseRepository<ViewMotionVote, MotionVote, object> {
|
||||||
|
/**
|
||||||
|
* @param DS DataStore access
|
||||||
|
* @param dataSend Sending data
|
||||||
|
* @param mapperService Map models to object
|
||||||
|
* @param viewModelStoreService Access view models
|
||||||
|
* @param translate Translate string
|
||||||
|
* @param httpService make HTTP Requests
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
DS: DataStoreService,
|
||||||
|
dataSend: DataSendService,
|
||||||
|
mapperService: CollectionStringMapperService,
|
||||||
|
viewModelStoreService: ViewModelStoreService,
|
||||||
|
translate: TranslateService,
|
||||||
|
relationManager: RelationManagerService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
DS,
|
||||||
|
dataSend,
|
||||||
|
mapperService,
|
||||||
|
viewModelStoreService,
|
||||||
|
translate,
|
||||||
|
relationManager,
|
||||||
|
MotionVote,
|
||||||
|
MotionVoteRelations
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTitle = (titleInformation: object) => {
|
||||||
|
return 'Vote';
|
||||||
|
};
|
||||||
|
|
||||||
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
|
return this.translate.instant(plural ? 'Votes' : 'Vote');
|
||||||
|
};
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { HttpService } from 'app/core/core-services/http.service';
|
import { HttpService } from 'app/core/core-services/http.service';
|
||||||
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
||||||
@ -135,4 +137,24 @@ export class ProjectorRepositoryService extends BaseRepository<ViewProjector, Pr
|
|||||||
public async setReferenceProjector(projector_id: number): Promise<void> {
|
public async setReferenceProjector(projector_id: number): Promise<void> {
|
||||||
await this.http.post<void>(`/rest/core/projector/${projector_id}/set_reference_projector/`);
|
await this.http.post<void>(`/rest/core/projector/${projector_id}/set_reference_projector/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return the id of the current reference projector
|
||||||
|
* prefer the observable whenever possible
|
||||||
|
*/
|
||||||
|
public getReferenceProjectorId(): number {
|
||||||
|
// TODO: After logging in, this is null this.getViewModelList() is null
|
||||||
|
return this.getViewModelList().find(projector => projector.isReferenceProjector).id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getReferenceProjectorIdObservable(): Observable<number> {
|
||||||
|
return this.getViewModelListObservable().pipe(
|
||||||
|
map(projectors => {
|
||||||
|
const refProjector = projectors.find(projector => projector.isReferenceProjector);
|
||||||
|
if (refProjector) {
|
||||||
|
return refProjector.id;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ describe('TagRepositoryService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
const service = TestBed.get(TagRepositoryService);
|
const service = TestBed.inject(TagRepositoryService);
|
||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -11,7 +11,7 @@ describe('TopicRepositoryService', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
const service: TopicRepositoryService = TestBed.get(TopicRepositoryService);
|
const service: TopicRepositoryService = TestBed.inject(TopicRepositoryService);
|
||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -10,6 +10,7 @@ import { ViewModelStoreService } from 'app/core/core-services/view-model-store.s
|
|||||||
import { RelationDefinition } from 'app/core/definitions/relations';
|
import { RelationDefinition } from 'app/core/definitions/relations';
|
||||||
import { Topic } from 'app/shared/models/topics/topic';
|
import { Topic } from 'app/shared/models/topics/topic';
|
||||||
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
||||||
|
import { CreateTopic } from 'app/site/topics/models/create-topic';
|
||||||
import { TopicTitleInformation, ViewTopic } from 'app/site/topics/models/view-topic';
|
import { TopicTitleInformation, ViewTopic } from 'app/site/topics/models/view-topic';
|
||||||
import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../base-is-agenda-item-and-list-of-speakers-content-object-repository';
|
import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../base-is-agenda-item-and-list-of-speakers-content-object-repository';
|
||||||
|
|
||||||
@ -52,33 +53,37 @@ export class TopicRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCon
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getTitle = (titleInformation: TopicTitleInformation) => {
|
public getTitle = (titleInformation: TopicTitleInformation) => {
|
||||||
|
return titleInformation.title;
|
||||||
|
};
|
||||||
|
|
||||||
|
public getListTitle = (titleInformation: TopicTitleInformation) => {
|
||||||
if (titleInformation.agenda_item_number && titleInformation.agenda_item_number()) {
|
if (titleInformation.agenda_item_number && titleInformation.agenda_item_number()) {
|
||||||
return `${titleInformation.agenda_item_number()} · ${titleInformation.title}`;
|
return `${titleInformation.agenda_item_number()} · ${titleInformation.title}`;
|
||||||
} else {
|
} else {
|
||||||
return titleInformation.title;
|
return this.getTitle(titleInformation);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public getAgendaListTitle = (titleInformation: TopicTitleInformation) => {
|
public getAgendaListTitle = (titleInformation: TopicTitleInformation) => {
|
||||||
// Do not append ' (Topic)' to the title.
|
return { title: this.getListTitle(titleInformation) };
|
||||||
return this.getTitle(titleInformation);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public getAgendaSlideTitle = (titleInformation: TopicTitleInformation) => {
|
public getAgendaSlideTitle = (titleInformation: TopicTitleInformation) => {
|
||||||
// Do not append ' (Topic)' to the title.
|
return this.getAgendaListTitle(titleInformation).title;
|
||||||
return this.getTitle(titleInformation);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @override The base function.
|
|
||||||
*
|
|
||||||
* @returns The plain title.
|
|
||||||
*/
|
|
||||||
public getAgendaListTitleWithoutItemNumber = (titleInformation: TopicTitleInformation) => {
|
|
||||||
return titleInformation.title;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public getVerboseName = (plural: boolean = false) => {
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
return this.translate.instant(plural ? 'Topics' : 'Topic');
|
return this.translate.instant(plural ? 'Topics' : 'Topic');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public duplicateTopic(topic: ViewTopic): void {
|
||||||
|
this.create(
|
||||||
|
new CreateTopic({
|
||||||
|
...topic.topic,
|
||||||
|
agenda_type: topic.item.type,
|
||||||
|
agenda_parent_id: topic.item.parent_id,
|
||||||
|
agenda_weight: topic.item.weight
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { HttpService } from 'app/core/core-services/http.service';
|
import { HttpService } from 'app/core/core-services/http.service';
|
||||||
|
import { Permission } from 'app/core/core-services/operator.service';
|
||||||
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
|
||||||
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||||
import { Group } from 'app/shared/models/users/group';
|
import { Group } from 'app/shared/models/users/group';
|
||||||
@ -16,9 +19,9 @@ import { DataStoreService } from '../../core-services/data-store.service';
|
|||||||
/**
|
/**
|
||||||
* Shape of a permission
|
* Shape of a permission
|
||||||
*/
|
*/
|
||||||
interface Permission {
|
interface PermDefinition {
|
||||||
display_name: string;
|
display_name: string;
|
||||||
value: string;
|
value: Permission;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,7 +29,7 @@ interface Permission {
|
|||||||
*/
|
*/
|
||||||
export interface AppPermissions {
|
export interface AppPermissions {
|
||||||
name: string;
|
name: string;
|
||||||
permissions: Permission[];
|
permissions: PermDefinition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,13 +75,20 @@ export class GroupRepositoryService extends BaseRepository<ViewGroup, Group, Gro
|
|||||||
return this.translate.instant(plural ? 'Groups' : 'Group');
|
return this.translate.instant(plural ? 'Groups' : 'Group');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public getNameForIds(...ids: number[]): string {
|
||||||
|
return this.getSortedViewModelList()
|
||||||
|
.filter(group => ids.includes(group.id))
|
||||||
|
.map(group => this.translate.instant(group.getTitle()))
|
||||||
|
.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles the given permisson.
|
* Toggles the given permisson.
|
||||||
*
|
*
|
||||||
* @param group The group
|
* @param group The group
|
||||||
* @param perm The permission to toggle
|
* @param perm The permission to toggle
|
||||||
*/
|
*/
|
||||||
public async togglePerm(group: ViewGroup, perm: string): Promise<void> {
|
public async togglePerm(group: ViewGroup, perm: Permission): Promise<void> {
|
||||||
const set = !group.permissions.includes(perm);
|
const set = !group.permissions.includes(perm);
|
||||||
return await this.http.post(`/rest/${group.collectionString}/${group.id}/set_permission/`, {
|
return await this.http.post(`/rest/${group.collectionString}/${group.id}/set_permission/`, {
|
||||||
perm: perm,
|
perm: perm,
|
||||||
@ -93,7 +103,7 @@ export class GroupRepositoryService extends BaseRepository<ViewGroup, Group, Gro
|
|||||||
* @param perm certain permission as string
|
* @param perm certain permission as string
|
||||||
* @param appName Indicates the header in the Permission Matrix
|
* @param appName Indicates the header in the Permission Matrix
|
||||||
*/
|
*/
|
||||||
private addAppPerm(appId: number, perm: Permission, appName: string): void {
|
private addAppPerm(appId: number, perm: PermDefinition, appName: string): void {
|
||||||
if (!this.appPermissions[appId]) {
|
if (!this.appPermissions[appId]) {
|
||||||
this.appPermissions[appId] = {
|
this.appPermissions[appId] = {
|
||||||
name: appName,
|
name: appName,
|
||||||
@ -187,4 +197,12 @@ export class GroupRepositoryService extends BaseRepository<ViewGroup, Group, Gro
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an Observable for all groups except the default group.
|
||||||
|
*/
|
||||||
|
public getViewModelListObservableWithoutDefaultGroup(): Observable<ViewGroup[]> {
|
||||||
|
// since groups are sorted by id, default is always the first entry
|
||||||
|
return this.getViewModelListObservable().pipe(map(groups => groups.slice(1)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,11 @@ import { DataSendService } from '../../core-services/data-send.service';
|
|||||||
import { DataStoreService } from '../../core-services/data-store.service';
|
import { DataStoreService } from '../../core-services/data-store.service';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
|
|
||||||
|
export interface MassImportResult {
|
||||||
|
importedTrackIds: number[];
|
||||||
|
errors: { [id: number]: string };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* type for determining the user name from a string during import.
|
* type for determining the user name from a string during import.
|
||||||
* See {@link parseUserString} for implementations
|
* See {@link parseUserString} for implementations
|
||||||
@ -125,6 +130,18 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
return name.trim();
|
return name.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getLevelAndNumber(titleInformation: UserTitleInformation): string {
|
||||||
|
if (titleInformation.structure_level && titleInformation.number) {
|
||||||
|
return `${titleInformation.structure_level} · ${this.translate.instant('No.')} ${titleInformation.number}`;
|
||||||
|
} else if (titleInformation.structure_level) {
|
||||||
|
return titleInformation.structure_level;
|
||||||
|
} else if (titleInformation.number) {
|
||||||
|
return `${this.translate.instant('No.')} ${titleInformation.number}`;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public getVerboseName = (plural: boolean = false) => {
|
public getVerboseName = (plural: boolean = false) => {
|
||||||
return this.translate.instant(plural ? 'Participants' : 'Participant');
|
return this.translate.instant(plural ? 'Participants' : 'Participant');
|
||||||
};
|
};
|
||||||
@ -145,12 +162,13 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds teh short and full name to the view user.
|
* Adds the short and full name to the view user.
|
||||||
*/
|
*/
|
||||||
protected createViewModelWithTitles(model: User): ViewUser {
|
protected createViewModelWithTitles(model: User): ViewUser {
|
||||||
const viewModel = super.createViewModelWithTitles(model);
|
const viewModel = super.createViewModelWithTitles(model);
|
||||||
viewModel.getFullName = () => this.getFullName(viewModel);
|
viewModel.getFullName = () => this.getFullName(viewModel);
|
||||||
viewModel.getShortName = () => this.getShortName(viewModel);
|
viewModel.getShortName = () => this.getShortName(viewModel);
|
||||||
|
viewModel.getLevelAndNumber = () => this.getLevelAndNumber(viewModel);
|
||||||
return viewModel;
|
return viewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,15 +227,11 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
*
|
*
|
||||||
* @param newEntries
|
* @param newEntries
|
||||||
*/
|
*/
|
||||||
public async bulkCreate(newEntries: NewEntry<User>[]): Promise<number[]> {
|
public async bulkCreate(newEntries: NewEntry<User>[]): Promise<MassImportResult> {
|
||||||
const data = newEntries.map(entry => {
|
const data = newEntries.map(entry => {
|
||||||
return { ...entry.newEntry, importTrackId: entry.importTrackId };
|
return { ...entry.newEntry, importTrackId: entry.importTrackId };
|
||||||
});
|
});
|
||||||
const response = (await this.httpService.post(`/rest/users/user/mass_import/`, { users: data })) as {
|
return await this.httpService.post<MassImportResult>(`/rest/users/user/mass_import/`, { users: data });
|
||||||
detail: string;
|
|
||||||
importedTrackIds: number[];
|
|
||||||
};
|
|
||||||
return response.importedTrackIds;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -293,7 +307,8 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
} else if (numEmails === 1) {
|
} else if (numEmails === 1) {
|
||||||
msg = this.translate.instant('One email was send sucessfully.');
|
msg = this.translate.instant('One email was send sucessfully.');
|
||||||
} else {
|
} else {
|
||||||
msg = this.translate.instant('%num% emails were send sucessfully.').replace('%num%', numEmails);
|
msg = this.translate.instant('%num% emails were send sucessfully.');
|
||||||
|
msg = msg.replace('%num%', numEmails);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (noEmailIds.length) {
|
if (noEmailIds.length) {
|
||||||
@ -375,7 +390,7 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
* @param schema optional hint on how to handle the strings.
|
* @param schema optional hint on how to handle the strings.
|
||||||
* @returns A User object (note: is only a local object, not uploaded to the server)
|
* @returns A User object (note: is only a local object, not uploaded to the server)
|
||||||
*/
|
*/
|
||||||
public parseUserString(inputUser: string, schema?: StringNamingSchema): User {
|
public parseUserString(inputUser: string, schema: StringNamingSchema = 'firstSpaceLast'): User {
|
||||||
const newUser: Partial<User> = {};
|
const newUser: Partial<User> = {};
|
||||||
if (schema === 'lastCommaFirst') {
|
if (schema === 'lastCommaFirst') {
|
||||||
const commaSeparated = inputUser.split(',');
|
const commaSeparated = inputUser.split(',');
|
||||||
@ -390,7 +405,7 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
|||||||
default:
|
default:
|
||||||
newUser.first_name = inputUser;
|
newUser.first_name = inputUser;
|
||||||
}
|
}
|
||||||
} else if (!schema || schema === 'firstSpaceLast') {
|
} else if (schema === 'firstSpaceLast') {
|
||||||
const splitUser = inputUser.split(' ');
|
const splitUser = inputUser.split(' ');
|
||||||
switch (splitUser.length) {
|
switch (splitUser.length) {
|
||||||
case 1:
|
case 1:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { _ } from 'app/core/translate/translation-marker';
|
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add strings here that require translations but have never been declared
|
* Add strings here that require translations but have never been declared
|
||||||
@ -30,6 +30,12 @@ _('Front page text');
|
|||||||
_('[Space for your welcome text.]');
|
_('[Space for your welcome text.]');
|
||||||
_('System');
|
_('System');
|
||||||
_('Allow access for anonymous guest users');
|
_('Allow access for anonymous guest users');
|
||||||
|
_('Show live conference window');
|
||||||
|
_('Connect all users to live conference automatically');
|
||||||
|
_('Allow only current speakers and list of speakers managers to enter the live conference');
|
||||||
|
_('Server settings required to activate Jitsi Meet integration.');
|
||||||
|
_('Livestream url');
|
||||||
|
_('Remove URL to deactivate livestream. Check extra group permission to see livestream.');
|
||||||
_('Show this text on the login page');
|
_('Show this text on the login page');
|
||||||
_('OpenSlides Theme');
|
_('OpenSlides Theme');
|
||||||
_('Export');
|
_('Export');
|
||||||
@ -58,6 +64,13 @@ _('PDF footer logo (left)');
|
|||||||
_('PDF footer logo (right)');
|
_('PDF footer logo (right)');
|
||||||
_('Web interface header logo');
|
_('Web interface header logo');
|
||||||
_('PDF ballot paper logo');
|
_('PDF ballot paper logo');
|
||||||
|
_('Foreground color');
|
||||||
|
_('Background color');
|
||||||
|
_('Header background color');
|
||||||
|
_('Header font color');
|
||||||
|
_('Headline color');
|
||||||
|
_('Chyron background color');
|
||||||
|
_('Chyron font color');
|
||||||
|
|
||||||
// Agenda config strings
|
// Agenda config strings
|
||||||
_('Enable numbering for agenda items');
|
_('Enable numbering for agenda items');
|
||||||
@ -86,7 +99,8 @@ _('Enter duration in seconds. Choose 0 to disable warning color.');
|
|||||||
_('Hide the amount of speakers in subtitle of list of speakers slide');
|
_('Hide the amount of speakers in subtitle of list of speakers slide');
|
||||||
_('Couple countdown with the list of speakers');
|
_('Couple countdown with the list of speakers');
|
||||||
_('[Begin speech] starts the countdown, [End speech] stops the countdown.');
|
_('[Begin speech] starts the countdown, [End speech] stops the countdown.');
|
||||||
_('Only present participants can be added to the list of speakers'), _('Agenda visibility');
|
_('Only present participants can be added to the list of speakers');
|
||||||
|
_('Show hint »first speech« in the list of speakers management view');
|
||||||
_('Default visibility for new agenda items (except topics)');
|
_('Default visibility for new agenda items (except topics)');
|
||||||
_('public');
|
_('public');
|
||||||
_('internal');
|
_('internal');
|
||||||
@ -99,7 +113,8 @@ _('Only main agenda items');
|
|||||||
_('Topics');
|
_('Topics');
|
||||||
_('Open requests to speak');
|
_('Open requests to speak');
|
||||||
|
|
||||||
// Motions config strings
|
// ** Motions **
|
||||||
|
// config strings
|
||||||
// subgroup general
|
// subgroup general
|
||||||
_('General');
|
_('General');
|
||||||
_('Workflow of new motions');
|
_('Workflow of new motions');
|
||||||
@ -155,7 +170,8 @@ _('Choose 0 to disable the supporting system.');
|
|||||||
_('Remove all supporters of a motion if a submitter edits his motion in early state');
|
_('Remove all supporters of a motion if a submitter edits his motion in early state');
|
||||||
// subgroup Voting and ballot papers
|
// subgroup Voting and ballot papers
|
||||||
_('Voting and ballot papers');
|
_('Voting and ballot papers');
|
||||||
_('The 100 % base of a voting result consists of');
|
_('Default voting type');
|
||||||
|
_('Default 100 % base of a voting result');
|
||||||
_('Yes/No/Abstain');
|
_('Yes/No/Abstain');
|
||||||
_('Yes/No');
|
_('Yes/No');
|
||||||
_('All valid ballots');
|
_('All valid ballots');
|
||||||
@ -176,12 +192,8 @@ _('Custom number of ballot papers');
|
|||||||
_('PDF export');
|
_('PDF export');
|
||||||
_('Title for PDF documents of motions');
|
_('Title for PDF documents of motions');
|
||||||
_('Preamble text for PDF documents of motions');
|
_('Preamble text for PDF documents of motions');
|
||||||
_('Show submitters and recommendation in table of contents');
|
_('Show submitters and recommendation/state in table of contents');
|
||||||
_('Show checkbox to record decision');
|
_('Show checkbox to record decision');
|
||||||
// misc motion strings
|
|
||||||
_('Amendment');
|
|
||||||
_('Statute amendment for');
|
|
||||||
_('Statute paragraphs');
|
|
||||||
|
|
||||||
// motion workflow 1
|
// motion workflow 1
|
||||||
_('Simple Workflow');
|
_('Simple Workflow');
|
||||||
@ -224,46 +236,7 @@ _('Needs review');
|
|||||||
_('rejected (not authorized)');
|
_('rejected (not authorized)');
|
||||||
_('Reject (not authorized)');
|
_('Reject (not authorized)');
|
||||||
_('Rejection (not authorized)');
|
_('Rejection (not authorized)');
|
||||||
// misc for motions
|
// motion workflow manager
|
||||||
_('Called');
|
|
||||||
_('Called with');
|
|
||||||
_('Recommendation');
|
|
||||||
_('Motion block');
|
|
||||||
_('The text field may not be blank.');
|
|
||||||
_('The reason field may not be blank.');
|
|
||||||
|
|
||||||
// Assignment config strings
|
|
||||||
_('Election method');
|
|
||||||
_('Automatic assign of method');
|
|
||||||
_('Always one option per candidate');
|
|
||||||
_('Always Yes-No-Abstain per candidate');
|
|
||||||
_('Always Yes/No per candidate');
|
|
||||||
_('Elections');
|
|
||||||
_('Ballot and ballot papers');
|
|
||||||
_('The 100-%-base of an election result consists of');
|
|
||||||
_(
|
|
||||||
'For Yes/No/Abstain per candidate and Yes/No per candidate the 100-%-base depends on the election method: If there is only one option per candidate, the sum of all votes of all candidates is 100 %. Otherwise for each candidate the sum of all votes is 100 %.'
|
|
||||||
);
|
|
||||||
_('Yes/No/Abstain per candidate');
|
|
||||||
_('Yes/No per candidate');
|
|
||||||
_('All valid ballots');
|
|
||||||
_('All casted ballots');
|
|
||||||
_('Disabled (no percents)');
|
|
||||||
_('Number of ballot papers (selection)');
|
|
||||||
_('Number of all delegates');
|
|
||||||
_('Number of all participants');
|
|
||||||
_('Use the following custom number');
|
|
||||||
_('Custom number of ballot papers');
|
|
||||||
_('Required majority');
|
|
||||||
_('Default method to check whether a candidate has reached the required majority.');
|
|
||||||
_('Simple majority');
|
|
||||||
_('Two-thirds majority');
|
|
||||||
_('Three-quarters majority');
|
|
||||||
_('Disabled');
|
|
||||||
_('Put all candidates on the list of speakers');
|
|
||||||
_('Title for PDF document (all elections)');
|
|
||||||
_('Preamble text for PDF document (all elections)');
|
|
||||||
// motion workflow
|
|
||||||
_('Recommendation label');
|
_('Recommendation label');
|
||||||
_('Allow support');
|
_('Allow support');
|
||||||
_('Allow create poll');
|
_('Allow create poll');
|
||||||
@ -275,11 +248,67 @@ _('Show amendment in parent motion');
|
|||||||
_('Restrictions');
|
_('Restrictions');
|
||||||
_('Label color');
|
_('Label color');
|
||||||
_('Next states');
|
_('Next states');
|
||||||
|
_('grey');
|
||||||
|
_('red');
|
||||||
|
_('green');
|
||||||
|
_('lightblue');
|
||||||
|
_('yellow');
|
||||||
|
// misc for motions
|
||||||
|
_('Amendment');
|
||||||
|
_('Statute amendment for');
|
||||||
|
_('Statute paragraphs');
|
||||||
|
_('Called');
|
||||||
|
_('Called with');
|
||||||
|
_('Recommendation');
|
||||||
|
_('Motion block');
|
||||||
|
_('The text field may not be blank.');
|
||||||
|
_('The reason field may not be blank.');
|
||||||
|
|
||||||
// other translations
|
// ** Assignments **
|
||||||
|
// Assignment config strings
|
||||||
|
_('Elections');
|
||||||
|
// subgroup ballot
|
||||||
|
_('Default election method');
|
||||||
|
_('Default 100 % base of an election result');
|
||||||
|
_('All valid ballots');
|
||||||
|
_('All casted ballots');
|
||||||
|
_('Disabled (no percents)');
|
||||||
|
_('Default groups with voting rights');
|
||||||
|
_('Sort election results by amount of votes');
|
||||||
|
_('Put all candidates on the list of speakers');
|
||||||
|
// subgroup ballot papers
|
||||||
|
_('Ballot papers');
|
||||||
|
_('Number of ballot papers');
|
||||||
|
_('Number of all delegates');
|
||||||
|
_('Number of all participants');
|
||||||
|
_('Use the following custom number');
|
||||||
|
_('Custom number of ballot papers');
|
||||||
|
_('Required majority');
|
||||||
|
_('Default method to check whether a candidate has reached the required majority.');
|
||||||
|
_('Simple majority');
|
||||||
|
_('Two-thirds majority');
|
||||||
|
_('Three-quarters majority');
|
||||||
|
_('Disabled');
|
||||||
|
_('Title for PDF document (all elections)');
|
||||||
|
_('Preamble text for PDF document (all elections)');
|
||||||
|
// misc for assignments
|
||||||
_('Searching for candidates');
|
_('Searching for candidates');
|
||||||
_('Voting');
|
|
||||||
_('Finished');
|
_('Finished');
|
||||||
|
_('In the election process');
|
||||||
|
|
||||||
|
// Voting strings
|
||||||
|
_('Voting type');
|
||||||
|
_('analog');
|
||||||
|
_('nominal');
|
||||||
|
_('non-nominal');
|
||||||
|
_('Start voting');
|
||||||
|
_('Stop voting');
|
||||||
|
_('Publish');
|
||||||
|
_('Entitled to vote');
|
||||||
|
_('Voting method');
|
||||||
|
_('Amount of votes');
|
||||||
|
_('Motion votes');
|
||||||
|
_('Ballots');
|
||||||
|
|
||||||
// ** Users **
|
// ** Users **
|
||||||
// permission strings (see models.py of each Django app)
|
// permission strings (see models.py of each Django app)
|
||||||
@ -303,6 +332,7 @@ _('Can manage tags');
|
|||||||
_('Can manage configuration');
|
_('Can manage configuration');
|
||||||
_('Can manage logos and fonts');
|
_('Can manage logos and fonts');
|
||||||
_('Can see history');
|
_('Can see history');
|
||||||
|
_('Can see the live stream');
|
||||||
// mediafiles
|
// mediafiles
|
||||||
_('Can see the list of files');
|
_('Can see the list of files');
|
||||||
_('Can upload files');
|
_('Can upload files');
|
||||||
@ -318,9 +348,11 @@ _('Can see comments');
|
|||||||
_('Can manage comments');
|
_('Can manage comments');
|
||||||
_('Can manage motion metadata');
|
_('Can manage motion metadata');
|
||||||
_('Can create amendments');
|
_('Can create amendments');
|
||||||
|
_('Can manage motion polls');
|
||||||
|
|
||||||
// users
|
// users
|
||||||
_('Can see names of users');
|
_('Can see names of users');
|
||||||
_('Can see extra data of users (e.g. present and comment)');
|
_('Can see extra data of users (e.g. email and comment)');
|
||||||
_('Can manage users');
|
_('Can manage users');
|
||||||
_('Can change its own password');
|
_('Can change its own password');
|
||||||
|
|
||||||
@ -328,6 +360,9 @@ _('Can change its own password');
|
|||||||
_('General');
|
_('General');
|
||||||
_('Sort name of participants by');
|
_('Sort name of participants by');
|
||||||
_('Enable participant presence view');
|
_('Enable participant presence view');
|
||||||
|
_('Activate vote weight');
|
||||||
|
_('Allow users to set themselves as present');
|
||||||
|
_('e.g. for online meetings');
|
||||||
_('Participants');
|
_('Participants');
|
||||||
_('Given name');
|
_('Given name');
|
||||||
_('Surname');
|
_('Surname');
|
||||||
@ -356,7 +391,7 @@ _('OpenSlides access data');
|
|||||||
_('You can use {event_name} and {username} as placeholder.');
|
_('You can use {event_name} and {username} as placeholder.');
|
||||||
_('Email body');
|
_('Email body');
|
||||||
_(
|
_(
|
||||||
'Dear {name},\n\nthis is your OpenSlides login for the event {event_name}:\n\n {url}\n username: {username}\n password: {password}\n\nThis email was generated automatically.'
|
'Dear {name},\n\nthis is your personal OpenSlides login:\n\n {url}\n username: {username}\n password: {password}\n\nThis email was generated automatically.'
|
||||||
);
|
);
|
||||||
_('Use these placeholders: {name}, {event_name}, {url}, {username}, {password}. The url referrs to the system url.');
|
_('Use these placeholders: {name}, {event_name}, {url}, {username}, {password}. The url referrs to the system url.');
|
||||||
_(
|
_(
|
||||||
@ -392,7 +427,27 @@ _('OpenSlides is temporarily reset to following timestamp');
|
|||||||
_('Motion change recommendation created');
|
_('Motion change recommendation created');
|
||||||
_('Motion change recommendation updated');
|
_('Motion change recommendation updated');
|
||||||
_('Motion change recommendation deleted');
|
_('Motion change recommendation deleted');
|
||||||
|
_('Motion block set to');
|
||||||
|
_('Poll created');
|
||||||
|
_('Poll updated');
|
||||||
|
_('Poll deleted');
|
||||||
|
_('Comment {arg1} updated');
|
||||||
|
|
||||||
// core misc strings
|
// core misc strings
|
||||||
_('items per page');
|
_('items per page');
|
||||||
_('Tag');
|
_('Tag');
|
||||||
|
|
||||||
|
// strings which are not extracted as translateable strings from client code
|
||||||
|
_('Foreground color');
|
||||||
|
_('Background color');
|
||||||
|
_('Header background color');
|
||||||
|
_('Header font color');
|
||||||
|
_('Headline color');
|
||||||
|
_('Chyron background color');
|
||||||
|
_('Chyron font color');
|
||||||
|
_('Show full text');
|
||||||
|
_('Hide more text');
|
||||||
|
_('Show password');
|
||||||
|
_('Hide password');
|
||||||
|
_('result');
|
||||||
|
_('results');
|
||||||
|
@ -29,7 +29,7 @@ import { OpenSlidesTranslateService } from './translation-service';
|
|||||||
exports: [TranslatePipe, TranslateDirective]
|
exports: [TranslatePipe, TranslateDirective]
|
||||||
})
|
})
|
||||||
export class OpenSlidesTranslateModule {
|
export class OpenSlidesTranslateModule {
|
||||||
public static forRoot(): ModuleWithProviders {
|
public static forRoot(): ModuleWithProviders<TranslateModule> {
|
||||||
return {
|
return {
|
||||||
ngModule: TranslateModule,
|
ngModule: TranslateModule,
|
||||||
providers: [
|
providers: [
|
||||||
@ -46,7 +46,7 @@ export class OpenSlidesTranslateModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// no config store for child.
|
// no config store for child.
|
||||||
public static forChild(): ModuleWithProviders {
|
public static forChild(): ModuleWithProviders<TranslateModule> {
|
||||||
return {
|
return {
|
||||||
ngModule: TranslateModule,
|
ngModule: TranslateModule,
|
||||||
providers: [
|
providers: [
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
/**
|
|
||||||
* Mark strings as translateable for ng-translate-extract.
|
|
||||||
* Marked strings are added into template-en.pot by 'npm run extract'.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* _('translateable string');
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function _(str: string): string {
|
|
||||||
return str;
|
|
||||||
}
|
|
@ -39,7 +39,7 @@ export class OpenSlidesTranslateService extends TranslateService {
|
|||||||
@Inject(USE_DEFAULT_LANG) useDefaultLang: boolean = true,
|
@Inject(USE_DEFAULT_LANG) useDefaultLang: boolean = true,
|
||||||
@Inject(USE_STORE) isolate: boolean = false
|
@Inject(USE_STORE) isolate: boolean = false
|
||||||
) {
|
) {
|
||||||
super(store, currentLoader, compiler, parser, missingTranslationHandler, useDefaultLang, isolate);
|
super(store, currentLoader, compiler, parser, missingTranslationHandler, useDefaultLang, isolate, true, 'en');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
12
client/src/app/core/ui-services/banner.service.spec.ts
Normal file
12
client/src/app/core/ui-services/banner.service.spec.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { BannerService } from './banner.service';
|
||||||
|
|
||||||
|
describe('BannerService', () => {
|
||||||
|
beforeEach(() => TestBed.configureTestingModule({}));
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: BannerService = TestBed.inject(BannerService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
66
client/src/app/core/ui-services/banner.service.ts
Normal file
66
client/src/app/core/ui-services/banner.service.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
|
export interface BannerDefinition {
|
||||||
|
type?: string;
|
||||||
|
class?: string;
|
||||||
|
icon?: string;
|
||||||
|
text?: string;
|
||||||
|
subText?: string;
|
||||||
|
link?: string;
|
||||||
|
largerOnMobileView?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service handling the active banners at the top of the site. Banners are defined via a BannerDefinition
|
||||||
|
* and are removed by reference so the service adding a banner has to store the reference to remove it later
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class BannerService {
|
||||||
|
public activeBanners: BehaviorSubject<BannerDefinition[]> = new BehaviorSubject<BannerDefinition[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a banner to the list of active banners. Skip the banner if it's already in the list
|
||||||
|
* @param toAdd the banner to add
|
||||||
|
*/
|
||||||
|
public addBanner(toAdd: BannerDefinition): void {
|
||||||
|
if (!this.activeBanners.value.find(banner => banner === toAdd)) {
|
||||||
|
const newBanners = this.activeBanners.value.concat([toAdd]);
|
||||||
|
this.activeBanners.next(newBanners);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces a banner with another. Convenience method to prevent flickering
|
||||||
|
* @param toAdd the banner to add
|
||||||
|
* @param toRemove the banner to remove
|
||||||
|
*/
|
||||||
|
public replaceBanner(toRemove: BannerDefinition, toAdd: BannerDefinition): void {
|
||||||
|
if (toRemove) {
|
||||||
|
const newArray = Array.from(this.activeBanners.value);
|
||||||
|
const idx = newArray.findIndex(banner => banner === toRemove);
|
||||||
|
if (idx === -1) {
|
||||||
|
throw new Error("The given banner couldn't be found.");
|
||||||
|
} else {
|
||||||
|
newArray[idx] = toAdd;
|
||||||
|
this.activeBanners.next(newArray); // no need for this.update since the length doesn't change
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.addBanner(toAdd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* removes the given banner
|
||||||
|
* @param toRemove the banner to remove
|
||||||
|
*/
|
||||||
|
public removeBanner(toRemove: BannerDefinition): void {
|
||||||
|
if (toRemove) {
|
||||||
|
const newBanners = this.activeBanners.value.filter(banner => banner !== toRemove);
|
||||||
|
this.activeBanners.next(newBanners);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -275,7 +275,8 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to get the `viewModelListObservable` of a given repository object and creates dynamic filters for them
|
* Helper function to get the `viewModelListObservable` of a given repository object and creates dynamic
|
||||||
|
* filters for them
|
||||||
*
|
*
|
||||||
* @param repo repository to create dynamic filters from
|
* @param repo repository to create dynamic filters from
|
||||||
* @param filter the OSFilter for the filter property
|
* @param filter the OSFilter for the filter property
|
||||||
@ -534,6 +535,8 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
|
|||||||
if (item[filter.property].id === option.condition) {
|
if (item[filter.property].id === option.condition) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
} else if (typeof item[filter.property] === 'function') {
|
||||||
|
return item[filter.property]() === option.condition;
|
||||||
} else if (item[filter.property] === option.condition) {
|
} else if (item[filter.property] === option.condition) {
|
||||||
return true;
|
return true;
|
||||||
} else if (item[filter.property].toString() === option.condition) {
|
} else if (item[filter.property].toString() === option.condition) {
|
||||||
|
@ -26,7 +26,7 @@ export interface NewEntry<V> {
|
|||||||
newEntry: V;
|
newEntry: V;
|
||||||
status: CsvImportStatus;
|
status: CsvImportStatus;
|
||||||
errors: string[];
|
errors: string[];
|
||||||
hasDuplicates: boolean;
|
hasDuplicates?: boolean;
|
||||||
importTrackId?: number;
|
importTrackId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,7 +179,6 @@ export abstract class BaseImportService<M extends BaseModel> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears all stored secondary data
|
* Clears all stored secondary data
|
||||||
* TODO: Merge with clearPreview()
|
|
||||||
*/
|
*/
|
||||||
public abstract clearData(): void;
|
public abstract clearData(): void;
|
||||||
|
|
||||||
@ -190,7 +189,6 @@ export abstract class BaseImportService<M extends BaseModel> {
|
|||||||
* @param file
|
* @param file
|
||||||
*/
|
*/
|
||||||
public parseInput(file: string): void {
|
public parseInput(file: string): void {
|
||||||
this.clearData();
|
|
||||||
this.clearPreview();
|
this.clearPreview();
|
||||||
const papaConfig: ParseConfig = {
|
const papaConfig: ParseConfig = {
|
||||||
header: false,
|
header: false,
|
||||||
@ -205,28 +203,7 @@ export abstract class BaseImportService<M extends BaseModel> {
|
|||||||
if (!valid) {
|
if (!valid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
entryLines.forEach(line => {
|
this._entries = entryLines.map(x => this.mapData(x)).filter(x => !!x);
|
||||||
const item = this.mapData(line);
|
|
||||||
if (item) {
|
|
||||||
this._entries.push(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.newEntries.next(this._entries);
|
|
||||||
this.updatePreview();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* parses pre-prepared entries (e.g. from a textarea) instead of a csv structure
|
|
||||||
*
|
|
||||||
* @param entries: an array of prepared newEntry objects
|
|
||||||
*/
|
|
||||||
public setParsedEntries(entries: NewEntry<M>[]): void {
|
|
||||||
this.clearData();
|
|
||||||
this.clearPreview();
|
|
||||||
if (!entries) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._entries = entries;
|
|
||||||
this.newEntries.next(this._entries);
|
this.newEntries.next(this._entries);
|
||||||
this.updatePreview();
|
this.updatePreview();
|
||||||
}
|
}
|
||||||
@ -238,6 +215,21 @@ export abstract class BaseImportService<M extends BaseModel> {
|
|||||||
*/
|
*/
|
||||||
public abstract mapData(line: string): NewEntry<M>;
|
public abstract mapData(line: string): NewEntry<M>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* parses pre-prepared entries (e.g. from a textarea) instead of a csv structure
|
||||||
|
*
|
||||||
|
* @param entries: an array of prepared newEntry objects
|
||||||
|
*/
|
||||||
|
public setParsedEntries(entries: NewEntry<M>[]): void {
|
||||||
|
this.clearPreview();
|
||||||
|
if (!entries) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._entries = entries;
|
||||||
|
this.newEntries.next(this._entries);
|
||||||
|
this.updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger for executing the import.
|
* Trigger for executing the import.
|
||||||
*/
|
*/
|
||||||
@ -293,7 +285,7 @@ export abstract class BaseImportService<M extends BaseModel> {
|
|||||||
// TODO: error message for wrong file type (test Firefox on Windows!)
|
// TODO: error message for wrong file type (test Firefox on Windows!)
|
||||||
if (event.target.files && event.target.files.length === 1) {
|
if (event.target.files && event.target.files.length === 1) {
|
||||||
this._rawFile = event.target.files[0];
|
this._rawFile = event.target.files[0];
|
||||||
this.readFile(event.target.files[0]);
|
this.readFile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,15 +295,15 @@ export abstract class BaseImportService<M extends BaseModel> {
|
|||||||
*/
|
*/
|
||||||
public refreshFile(): void {
|
public refreshFile(): void {
|
||||||
if (this._rawFile) {
|
if (this._rawFile) {
|
||||||
this.readFile(this._rawFile);
|
this.readFile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (re)-reads a given file with the current parameter
|
* reads the _rawFile
|
||||||
*/
|
*/
|
||||||
private readFile(file: File): void {
|
private readFile(): void {
|
||||||
this.reader.readAsText(file, this.encoding);
|
this.reader.readAsText(this._rawFile, this.encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -349,6 +341,7 @@ export abstract class BaseImportService<M extends BaseModel> {
|
|||||||
* Resets the data and preview (triggered upon selecting an invalid file)
|
* Resets the data and preview (triggered upon selecting an invalid file)
|
||||||
*/
|
*/
|
||||||
public clearPreview(): void {
|
public clearPreview(): void {
|
||||||
|
this.clearData();
|
||||||
this._entries = [];
|
this._entries = [];
|
||||||
this.newEntries.next([]);
|
this.newEntries.next([]);
|
||||||
this._preview = null;
|
this._preview = null;
|
||||||
@ -358,7 +351,7 @@ export abstract class BaseImportService<M extends BaseModel> {
|
|||||||
* set a list of short names for error, indicating which column failed
|
* set a list of short names for error, indicating which column failed
|
||||||
*/
|
*/
|
||||||
public setError(entry: NewEntry<M>, error: string): void {
|
public setError(entry: NewEntry<M>, error: string): void {
|
||||||
if (this.errorList.hasOwnProperty(error)) {
|
if (this.errorList[error]) {
|
||||||
if (!entry.errors) {
|
if (!entry.errors) {
|
||||||
entry.errors = [error];
|
entry.errors = [error];
|
||||||
} else if (!entry.errors.includes(error)) {
|
} else if (!entry.errors.includes(error)) {
|
||||||
|
60
client/src/app/core/ui-services/base-poll-dialog.service.ts
Normal file
60
client/src/app/core/ui-services/base-poll-dialog.service.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { ComponentType } from '@angular/cdk/portal';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
|
||||||
|
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
|
||||||
|
import { Collection } from 'app/shared/models/base/collection';
|
||||||
|
import { PollState, PollType } from 'app/shared/models/poll/base-poll';
|
||||||
|
import { mediumDialogSettings } from 'app/shared/utils/dialog-settings';
|
||||||
|
import { BasePollDialogComponent } from 'app/site/polls/components/base-poll-dialog.component';
|
||||||
|
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||||
|
import { PollService } from 'app/site/polls/services/poll.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class for showing a poll dialog. Has to be subclassed to provide the right `PollService`
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export abstract class BasePollDialogService<V extends ViewBasePoll, S extends PollService> {
|
||||||
|
protected dialogComponent: ComponentType<BasePollDialogComponent<V, S>>;
|
||||||
|
|
||||||
|
public constructor(private dialog: MatDialog, private mapper: CollectionStringMapperService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the dialog to enter votes and edit the meta-info for a poll.
|
||||||
|
*
|
||||||
|
* @param data Passing the (existing or new) data for the poll
|
||||||
|
*/
|
||||||
|
public async openDialog(viewPoll: Partial<V> & Collection): Promise<void> {
|
||||||
|
const dialogRef = this.dialog.open(this.dialogComponent, {
|
||||||
|
data: viewPoll,
|
||||||
|
...mediumDialogSettings
|
||||||
|
});
|
||||||
|
const result = await dialogRef.afterClosed().toPromise();
|
||||||
|
if (result) {
|
||||||
|
const repo = this.mapper.getRepository(viewPoll.collectionString);
|
||||||
|
if (!viewPoll.poll) {
|
||||||
|
await repo.create(result);
|
||||||
|
} else {
|
||||||
|
let update = result;
|
||||||
|
if (viewPoll.state !== PollState.Created) {
|
||||||
|
update = {
|
||||||
|
title: result.title,
|
||||||
|
onehundred_percent_base: result.onehundred_percent_base,
|
||||||
|
majority_method: result.majority_method,
|
||||||
|
description: result.description
|
||||||
|
};
|
||||||
|
if (viewPoll.type === PollType.Analog) {
|
||||||
|
update = {
|
||||||
|
...update,
|
||||||
|
votes: result.votes,
|
||||||
|
publish_immediately: result.publish_immediately
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await repo.patch(update, <V>viewPoll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,7 @@ describe('BaseSortService', () => {
|
|||||||
|
|
||||||
// TODO testing (does not work without injecting a BaseViewComponent)
|
// TODO testing (does not work without injecting a BaseViewComponent)
|
||||||
// it('should be created', () => {
|
// it('should be created', () => {
|
||||||
// const service: BaseSortService = TestBed.get(BaseSortService);
|
// const service: BaseSortService = TestBed.inject(BaseSortService);
|
||||||
// expect(service).toBeTruthy();
|
// expect(service).toBeTruthy();
|
||||||
// });
|
// });
|
||||||
});
|
});
|
||||||
|
@ -13,7 +13,7 @@ describe('ChoiceService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
const service: ChoiceService = TestBed.get(ChoiceService);
|
const service: ChoiceService = TestBed.inject(ChoiceService);
|
||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -11,7 +11,6 @@ interface CountUserRequest {
|
|||||||
|
|
||||||
export interface CountUserData {
|
export interface CountUserData {
|
||||||
userId: number;
|
userId: number;
|
||||||
usesIndexedDB: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CountUserResponse extends CountUserRequest {
|
interface CountUserResponse extends CountUserRequest {
|
||||||
@ -49,8 +48,7 @@ export class CountUsersService {
|
|||||||
{
|
{
|
||||||
token: request.content.token,
|
token: request.content.token,
|
||||||
data: {
|
data: {
|
||||||
userId: this.currentUserId,
|
userId: this.currentUserId
|
||||||
usesIndexedDB: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
request.senderChannelName
|
request.senderChannelName
|
||||||
|
@ -754,7 +754,10 @@ describe('DiffService', () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
it('handles inserted paragraphs (2)', inject([DiffService], (service: DiffService) => {
|
it('handles inserted paragraphs (2)', inject([DiffService], (service: DiffService) => {
|
||||||
// Specifically, Noch</p> should not be enclosed by <ins>...</ins>, as <ins>Noch </p></ins> would be seriously broken
|
/**
|
||||||
|
* Specifically, Noch</p> should not be enclosed by <ins>...</ins>, as <ins>Noch </p></ins>
|
||||||
|
* would be seriously broken
|
||||||
|
*/
|
||||||
const before =
|
const before =
|
||||||
"<P>rief sie alle sieben herbei und sprach 'liebe Kinder, ich will hinaus in den Wald, seid </P>",
|
"<P>rief sie alle sieben herbei und sprach 'liebe Kinder, ich will hinaus in den Wald, seid </P>",
|
||||||
after =
|
after =
|
||||||
@ -1169,6 +1172,28 @@ describe('DiffService', () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
||||||
|
it('detects a word replacement at the end of line correctly', inject([DiffService], (service: DiffService) => {
|
||||||
|
const before =
|
||||||
|
'<p>' +
|
||||||
|
noMarkup(1) +
|
||||||
|
'wuid Brotzeit? Pfenningguat Stubn bitt da, hog di hi fei nia need nia need Goaßmaß ' +
|
||||||
|
brMarkup(2) +
|
||||||
|
'gscheid kloan mim';
|
||||||
|
const after =
|
||||||
|
'<P>wuid Brotzeit? Pfenningguat Stubn bitt da, ' +
|
||||||
|
'hog di hi fei nia need nia need Radler gscheid kloan mim';
|
||||||
|
|
||||||
|
const diff = service.diff(before, after);
|
||||||
|
expect(diff).toBe(
|
||||||
|
'<p>' +
|
||||||
|
noMarkup(1) +
|
||||||
|
'wuid Brotzeit? Pfenningguat Stubn bitt da, ' +
|
||||||
|
'hog di hi fei nia need nia need <del>Goaßmaß </del><ins>Radler </ins>' +
|
||||||
|
brMarkup(2) +
|
||||||
|
'gscheid kloan mim</p>'
|
||||||
|
);
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('addCSSClassToFirstTag function', () => {
|
describe('addCSSClassToFirstTag function', () => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { LinenumberingService } from './linenumbering.service';
|
import { LineNumberedString, LinenumberingService } from './linenumbering.service';
|
||||||
import { ViewUnifiedChange } from '../../shared/models/motions/view-unified-change';
|
import { ViewUnifiedChange } from '../../shared/models/motions/view-unified-change';
|
||||||
|
|
||||||
const ELEMENT_NODE = 1;
|
const ELEMENT_NODE = 1;
|
||||||
@ -25,7 +25,8 @@ export enum ModificationType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This data structure is used when determining the most specific common ancestor of two HTML nodes (`node1` and `node2`)
|
* This data structure is used when determining the most specific common ancestor of two HTML node
|
||||||
|
* (`node1` and `node2`)
|
||||||
* within the same Document Fragment.
|
* within the same Document Fragment.
|
||||||
*/
|
*/
|
||||||
interface CommonAncestorData {
|
interface CommonAncestorData {
|
||||||
@ -34,11 +35,13 @@ interface CommonAncestorData {
|
|||||||
*/
|
*/
|
||||||
commonAncestor: Node;
|
commonAncestor: Node;
|
||||||
/**
|
/**
|
||||||
* The nodes inbetween `commonAncestor` and the `node1` in the DOM hierarchy. Empty, if node1 is a direct descendant.
|
* The nodes inbetween `commonAncestor` and the `node1` in the DOM hierarchy.
|
||||||
|
* Empty, if node1 is a direct descendant.
|
||||||
*/
|
*/
|
||||||
trace1: Node[];
|
trace1: Node[];
|
||||||
/**
|
/**
|
||||||
* The nodes inbetween `commonAncestor` and the `node2` in the DOM hierarchy. Empty, if node2 is a direct descendant.
|
* The nodes inbetween `commonAncestor` and the `node2` in the DOM hierarchy.
|
||||||
|
* Empty, if node2 is a direct descendant.
|
||||||
*/
|
*/
|
||||||
trace2: Node[];
|
trace2: Node[];
|
||||||
/**
|
/**
|
||||||
@ -109,7 +112,8 @@ export interface LineRange {
|
|||||||
/**
|
/**
|
||||||
* The end line number.
|
* The end line number.
|
||||||
* HINT: As this object is usually referring to actual line numbers, not lines,
|
* HINT: As this object is usually referring to actual line numbers, not lines,
|
||||||
* the line starting by `to` is not included in the extracted content anymore, only the text between `from` and `to`.
|
* the line starting by `to` is not included in the extracted content anymore,
|
||||||
|
* only the text between `from` and `to`.
|
||||||
*/
|
*/
|
||||||
to: number;
|
to: number;
|
||||||
}
|
}
|
||||||
@ -167,7 +171,9 @@ export interface DiffLinesInParagraph {
|
|||||||
*
|
*
|
||||||
* ```ts
|
* ```ts
|
||||||
* const lineLength = 80;
|
* const lineLength = 80;
|
||||||
* const lineNumberedText = this.lineNumbering.insertLineNumbers('<p>A line</p><p>Another line</p><ul><li>A list item</li><li>Yet another item</li></ul>', lineLength);
|
* const lineNumberedText = this.lineNumbering.insertLineNumbers(
|
||||||
|
* '<p>A line</p><p>Another line</p><ul><li>A list item</li><li>Yet another item</li></ul>', lineLength
|
||||||
|
* );
|
||||||
* const extractFrom = 2;
|
* const extractFrom = 2;
|
||||||
* const extractUntil = 3;
|
* const extractUntil = 3;
|
||||||
* const extractedData = this.diffService.extractRangeByLineNumbers(lineNumberedText, extractFrom, extractUntil)
|
* const extractedData = this.diffService.extractRangeByLineNumbers(lineNumberedText, extractFrom, extractUntil)
|
||||||
@ -197,7 +203,8 @@ export interface DiffLinesInParagraph {
|
|||||||
* Given a diff'ed string, apply all changes to receive the new version of the text:
|
* Given a diff'ed string, apply all changes to receive the new version of the text:
|
||||||
*
|
*
|
||||||
* ```ts
|
* ```ts
|
||||||
* const diffedHtml = '<p>Test <span class="delete">Test 2</span> Another test <del>Test 3</del></p><p class="delete">Test 4</p>';
|
* const diffedHtml =
|
||||||
|
* '<p>Test <span class="delete">Test 2</span> Another test <del>Test 3</del></p><p class="delete">Test 4</p>';
|
||||||
* const newVersion = this.diffService.diffHtmlToFinalText(diffedHtml);
|
* const newVersion = this.diffService.diffHtmlToFinalText(diffedHtml);
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
@ -205,7 +212,11 @@ export interface DiffLinesInParagraph {
|
|||||||
*
|
*
|
||||||
* ```ts
|
* ```ts
|
||||||
* const lineLength = 80;
|
* const lineLength = 80;
|
||||||
* const lineNumberedText = this.lineNumbering.insertLineNumbers('<p>A line</p><p>Another line</p><ul><li>A list item</li><li>Yet another item</li></ul>', lineLength);
|
* const lineNumberedText =
|
||||||
|
* this.lineNumbering.insertLineNumbers(
|
||||||
|
* '<p>A line</p><p>Another line</p><ul><li>A list item</li><li>Yet another item</li></ul>',
|
||||||
|
* lineLength
|
||||||
|
* );
|
||||||
* const merged = this.diffService.replaceLines(lineNumberedText, '<p>Replaced paragraph</p>', 1, 2);
|
* const merged = this.diffService.replaceLines(lineNumberedText, '<p>Replaced paragraph</p>', 1, 2);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@ -885,10 +896,7 @@ export class DiffService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return str
|
return str.replace(/^\s+/g, '').replace(/\s+$/g, '').replace(/ {2,}/g, ' ');
|
||||||
.replace(/^\s+/g, '')
|
|
||||||
.replace(/\s+$/g, '')
|
|
||||||
.replace(/ {2,}/g, ' ');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1000,14 +1008,7 @@ export class DiffService {
|
|||||||
classes = childElement.getAttribute('class').split(' ');
|
classes = childElement.getAttribute('class').split(' ');
|
||||||
}
|
}
|
||||||
classes.push(className);
|
classes.push(className);
|
||||||
childElement.setAttribute(
|
childElement.setAttribute('class', classes.sort().join(' ').replace(/^\s+/, '').replace(/\s+$/, ''));
|
||||||
'class',
|
|
||||||
classes
|
|
||||||
.sort()
|
|
||||||
.join(' ')
|
|
||||||
.replace(/^\s+/, '')
|
|
||||||
.replace(/\s+$/, '')
|
|
||||||
);
|
|
||||||
foundLast = true;
|
foundLast = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1073,7 +1074,8 @@ export class DiffService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This fixes a very specific, really weird bug that is tested in the test case "does not a change in a very specific case".
|
* This fixes a very specific, really weird bug that is tested in the test case "does not a change in a very
|
||||||
|
* specific case.
|
||||||
*
|
*
|
||||||
* @param {string}diffStr
|
* @param {string}diffStr
|
||||||
* @return {string}
|
* @return {string}
|
||||||
@ -1150,10 +1152,7 @@ export class DiffService {
|
|||||||
let html = this.serializeTag(node);
|
let html = this.serializeTag(node);
|
||||||
for (let i = 0; i < node.childNodes.length; i++) {
|
for (let i = 0; i < node.childNodes.length; i++) {
|
||||||
if (node.childNodes[i].nodeType === TEXT_NODE) {
|
if (node.childNodes[i].nodeType === TEXT_NODE) {
|
||||||
html += node.childNodes[i].nodeValue
|
html += node.childNodes[i].nodeValue.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>');
|
|
||||||
} else if (
|
} else if (
|
||||||
!stripLineNumbers ||
|
!stripLineNumbers ||
|
||||||
(!this.lineNumberingService.isOsLineNumberNode(node.childNodes[i]) &&
|
(!this.lineNumberingService.isOsLineNumberNode(node.childNodes[i]) &&
|
||||||
@ -1191,7 +1190,8 @@ export class DiffService {
|
|||||||
/**
|
/**
|
||||||
* Given a DOM tree and a specific node within that tree, this method returns the HTML string from the beginning
|
* Given a DOM tree and a specific node within that tree, this method returns the HTML string from the beginning
|
||||||
* of this tree up to this node.
|
* of this tree up to this node.
|
||||||
* The returned string in itself is not renderable, as it stops in the middle of the complete HTML, with opened tags.
|
* The returned string in itself is not renderable, as it stops in the middle of the complete HTML, with
|
||||||
|
* opened tags.
|
||||||
*
|
*
|
||||||
* Implementation hint: the first element of "toChildTrace" array needs to be a child element of "node"
|
* Implementation hint: the first element of "toChildTrace" array needs to be a child element of "node"
|
||||||
* @param {Node} node
|
* @param {Node} node
|
||||||
@ -1241,7 +1241,8 @@ export class DiffService {
|
|||||||
/**
|
/**
|
||||||
* Given a DOM tree and a specific node within that tree, this method returns the HTML string beginning after this
|
* Given a DOM tree and a specific node within that tree, this method returns the HTML string beginning after this
|
||||||
* node to the end of the tree.
|
* node to the end of the tree.
|
||||||
* The returned string in itself is not renderable, as it starts in the middle of the complete HTML, with opened tags.
|
* The returned string in itself is not renderable, as it starts in the middle of the complete HTML
|
||||||
|
* with opened tags.
|
||||||
*
|
*
|
||||||
* Implementation hint: the first element of "fromChildTrace" array needs to be a child element of "node"
|
* Implementation hint: the first element of "fromChildTrace" array needs to be a child element of "node"
|
||||||
* @param {Node} node
|
* @param {Node} node
|
||||||
@ -1296,7 +1297,8 @@ export class DiffService {
|
|||||||
* Returns the HTML snippet between two given line numbers.
|
* Returns the HTML snippet between two given line numbers.
|
||||||
* extractRangeByLineNumbers
|
* extractRangeByLineNumbers
|
||||||
* Hint:
|
* Hint:
|
||||||
* - The last line (toLine) is not included anymore, as the number refers to the line breaking element at the end of the line
|
* - The last line (toLine) is not included anymore, as the number refers to the line breaking element at the end
|
||||||
|
* of the line
|
||||||
* - if toLine === null, then everything from fromLine to the end of the fragment is returned
|
* - if toLine === null, then everything from fromLine to the end of the fragment is returned
|
||||||
*
|
*
|
||||||
* In addition to the HTML snippet, additional information is provided regarding the most specific DOM element
|
* In addition to the HTML snippet, additional information is provided regarding the most specific DOM element
|
||||||
@ -1309,18 +1311,19 @@ export class DiffService {
|
|||||||
* rendering it and for merging it again correctly.
|
* rendering it and for merging it again correctly.
|
||||||
* - os-split-*: These classes are set for all HTML Tags that have been split into two by this process,
|
* - os-split-*: These classes are set for all HTML Tags that have been split into two by this process,
|
||||||
* e.g. if the fromLine- or toLine-line-break was somewhere in the middle of this tag.
|
* e.g. if the fromLine- or toLine-line-break was somewhere in the middle of this tag.
|
||||||
* If a tag is split, the first one receives "os-split-after", and the second one "os-split-before".
|
* If a tag is split, the first one receives "os-split-after", and the second
|
||||||
|
* one "os-split-before".
|
||||||
* For example, for the following string <p>Line 1<br>Line 2<br>Line 3</p>:
|
* For example, for the following string <p>Line 1<br>Line 2<br>Line 3</p>:
|
||||||
* - extracting line 1 to 2 results in <p class="os-split-after">Line 1</p>
|
* - extracting line 1 to 2 results in <p class="os-split-after">Line 1</p>
|
||||||
* - extracting line 2 to 3 results in <p class="os-split-after os-split-before">Line 2</p>
|
* - extracting line 2 to 3 results in <p class="os-split-after os-split-before">Line 2</p>
|
||||||
* - extracting line 3 to null/4 results in <p class="os-split-before">Line 3</p>
|
* - extracting line 3 to null/4 results in <p class="os-split-before">Line 3</p>
|
||||||
*
|
*
|
||||||
* @param {string} htmlIn
|
* @param {LineNumberedString} htmlIn
|
||||||
* @param {number} fromLine
|
* @param {number} fromLine
|
||||||
* @param {number} toLine
|
* @param {number} toLine
|
||||||
* @returns {ExtractedContent}
|
* @returns {ExtractedContent}
|
||||||
*/
|
*/
|
||||||
public extractRangeByLineNumbers(htmlIn: string, fromLine: number, toLine: number): ExtractedContent {
|
public extractRangeByLineNumbers(htmlIn: LineNumberedString, fromLine: number, toLine: number): ExtractedContent {
|
||||||
if (typeof htmlIn !== 'string') {
|
if (typeof htmlIn !== 'string') {
|
||||||
throw new Error('Invalid call - extractRangeByLineNumbers expects a string as first argument');
|
throw new Error('Invalid call - extractRangeByLineNumbers expects a string as first argument');
|
||||||
}
|
}
|
||||||
@ -1601,7 +1604,8 @@ export class DiffService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This returns the line number range in which changes (insertions, deletions) are encountered.
|
* This returns the line number range in which changes (insertions, deletions) are encountered.
|
||||||
* As in extractRangeByLineNumbers(), "to" refers to the line breaking element at the end, i.e. the start of the following line.
|
* As in extractRangeByLineNumbers(), "to" refers to the line breaking element at the end, i.e. the start of the
|
||||||
|
* following line.
|
||||||
*
|
*
|
||||||
* @param {string} diffHtml
|
* @param {string} diffHtml
|
||||||
* @returns {LineRange}
|
* @returns {LineRange}
|
||||||
@ -1867,25 +1871,35 @@ export class DiffService {
|
|||||||
|
|
||||||
// Performing the actual diff
|
// Performing the actual diff
|
||||||
const str = this.diffString(workaroundPrepend + htmlOld, workaroundPrepend + htmlNew);
|
const str = this.diffString(workaroundPrepend + htmlOld, workaroundPrepend + htmlNew);
|
||||||
let diffUnnormalized = str
|
let diffUnnormalized = str.replace(/^\s+/g, '').replace(/\s+$/g, '').replace(/ {2,}/g, ' ');
|
||||||
.replace(/^\s+/g, '')
|
|
||||||
.replace(/\s+$/g, '')
|
|
||||||
.replace(/ {2,}/g, ' ');
|
|
||||||
|
|
||||||
diffUnnormalized = this.fixWrongChangeDetection(diffUnnormalized);
|
diffUnnormalized = this.fixWrongChangeDetection(diffUnnormalized);
|
||||||
|
|
||||||
// Remove <del> tags that only delete line numbers
|
// Remove <del> tags that only delete line numbers
|
||||||
// We need to do this before removing </del><del> as done in one of the next statements
|
// We need to do this before removing </del><del> as done in one of the next statements
|
||||||
diffUnnormalized = diffUnnormalized.replace(
|
diffUnnormalized = diffUnnormalized.replace(
|
||||||
/<del>((<BR CLASS="os-line-break"><\/del><del>)?(<span[^>]+os-line-number[^>]+?>)(\s|<\/?del>)*<\/span>)<\/del>/gi,
|
/<del>(((<BR CLASS="os-line-break">)<\/del><del>)?(<span[^>]+os-line-number[^>]+?>)(\s|<\/?del>)*<\/span>)<\/del>/gi,
|
||||||
(found: string, tag: string, br: string, span: string): string => {
|
(found: string, tag: string, brWithDel: string, plainBr: string, span: string): string => {
|
||||||
return (br !== undefined ? br : '') + span + ' </span>';
|
return (plainBr !== undefined ? plainBr : '') + span + ' </span>';
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Merging individual insert/delete statements into bigger blocks
|
// Merging individual insert/delete statements into bigger blocks
|
||||||
diffUnnormalized = diffUnnormalized.replace(/<\/ins><ins>/gi, '').replace(/<\/del><del>/gi, '');
|
diffUnnormalized = diffUnnormalized.replace(/<\/ins><ins>/gi, '').replace(/<\/del><del>/gi, '');
|
||||||
|
|
||||||
|
// If we have a <del>deleted word</del>LINEBREAK<ins>new word</ins>, let's assume that the insertion
|
||||||
|
// was actually done in the same line as the deletion.
|
||||||
|
// We don't have the LINEBREAK-markers in the new string, hence we can't be a 100% sure, but
|
||||||
|
// this will probably the more frequent case.
|
||||||
|
// This only really makes a differences for change recommendations anyway, where we split the text into lines
|
||||||
|
// Hint: if there is no deletion before the line break, we have the same issue, but cannot solve this here.
|
||||||
|
diffUnnormalized = diffUnnormalized.replace(
|
||||||
|
/(<\/del>)(<BR CLASS="os-line-break"><span[^>]+os-line-number[^>]+?>\s*<\/span>)(<ins>[\s\S]*?<\/ins>)/gi,
|
||||||
|
(found: string, del: string, br: string, ins: string): string => {
|
||||||
|
return del + ins + br;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// If only a few characters of a word have changed, don't display this as a replacement of the whole word,
|
// If only a few characters of a word have changed, don't display this as a replacement of the whole word,
|
||||||
// but only of these specific characters
|
// but only of these specific characters
|
||||||
diffUnnormalized = diffUnnormalized.replace(
|
diffUnnormalized = diffUnnormalized.replace(
|
||||||
@ -2116,8 +2130,10 @@ export class DiffService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
changes.forEach((change: ViewUnifiedChange) => {
|
changes.forEach((change: ViewUnifiedChange) => {
|
||||||
html = this.lineNumberingService.insertLineNumbers(html, lineLength, null, null, 1);
|
if (!change.isTitleChange()) {
|
||||||
html = this.replaceLines(html, change.getChangeNewText(), change.getLineFrom(), change.getLineTo());
|
html = this.lineNumberingService.insertLineNumbers(html, lineLength, null, null, 1);
|
||||||
|
html = this.replaceLines(html, change.getChangeNewText(), change.getLineFrom(), change.getLineTo());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
html = this.lineNumberingService.insertLineNumbers(html, lineLength, highlightLine, null, 1);
|
html = this.lineNumberingService.insertLineNumbers(html, lineLength, highlightLine, null, 1);
|
||||||
@ -2135,7 +2151,7 @@ export class DiffService {
|
|||||||
* @param {number} lineLength the line length
|
* @param {number} lineLength the line length
|
||||||
* @return {DiffLinesInParagraph|null}
|
* @return {DiffLinesInParagraph|null}
|
||||||
*/
|
*/
|
||||||
public getAmendmentParagraphsLinesByMode(
|
public getAmendmentParagraphsLines(
|
||||||
paragraphNo: number,
|
paragraphNo: number,
|
||||||
origText: string,
|
origText: string,
|
||||||
newText: string,
|
newText: string,
|
||||||
@ -2187,20 +2203,18 @@ export class DiffService {
|
|||||||
* Returns the HTML with the changes, optionally with a highlighted line.
|
* Returns the HTML with the changes, optionally with a highlighted line.
|
||||||
* The original motion needs to be provided.
|
* The original motion needs to be provided.
|
||||||
*
|
*
|
||||||
* @param {string} motionHtml
|
* @param {LineNumberedString} html
|
||||||
* @param {ViewUnifiedChange} change
|
* @param {ViewUnifiedChange} change
|
||||||
* @param {number} lineLength
|
* @param {number} lineLength
|
||||||
* @param {number} highlight
|
* @param {number} highlight
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
public getChangeDiff(
|
public getChangeDiff(
|
||||||
motionHtml: string,
|
html: LineNumberedString,
|
||||||
change: ViewUnifiedChange,
|
change: ViewUnifiedChange,
|
||||||
lineLength: number,
|
lineLength: number,
|
||||||
highlight?: number
|
highlight?: number
|
||||||
): string {
|
): string {
|
||||||
const html = this.lineNumberingService.insertLineNumbers(motionHtml, lineLength);
|
|
||||||
|
|
||||||
let data, oldText;
|
let data, oldText;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -2245,14 +2259,14 @@ export class DiffService {
|
|||||||
/**
|
/**
|
||||||
* Returns the remainder text of the motion after the last change
|
* Returns the remainder text of the motion after the last change
|
||||||
*
|
*
|
||||||
* @param {string} motionHtml
|
* @param {LineNumberedString} motionHtml
|
||||||
* @param {ViewUnifiedChange[]} changes
|
* @param {ViewUnifiedChange[]} changes
|
||||||
* @param {number} lineLength
|
* @param {number} lineLength
|
||||||
* @param {number} highlight
|
* @param {number} highlight
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
public getTextRemainderAfterLastChange(
|
public getTextRemainderAfterLastChange(
|
||||||
motionHtml: string,
|
motionHtml: LineNumberedString,
|
||||||
changes: ViewUnifiedChange[],
|
changes: ViewUnifiedChange[],
|
||||||
lineLength: number,
|
lineLength: number,
|
||||||
highlight?: number
|
highlight?: number
|
||||||
@ -2264,15 +2278,14 @@ export class DiffService {
|
|||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
const numberedHtml = this.lineNumberingService.insertLineNumbers(motionHtml, lineLength, highlight);
|
|
||||||
if (changes.length === 0) {
|
if (changes.length === 0) {
|
||||||
return numberedHtml;
|
return motionHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
data = this.extractRangeByLineNumbers(numberedHtml, maxLine, null);
|
data = this.extractRangeByLineNumbers(motionHtml, maxLine, null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// This only happens (as far as we know) when the motion text has been altered (shortened)
|
// This only happens (as far as we know) when the motion text has been altered (shortened)
|
||||||
// without modifying the change recommendations accordingly.
|
// without modifying the change recommendations accordingly.
|
||||||
@ -2302,21 +2315,20 @@ export class DiffService {
|
|||||||
/**
|
/**
|
||||||
* Extracts a renderable HTML string representing the given line number range of this motion text
|
* Extracts a renderable HTML string representing the given line number range of this motion text
|
||||||
*
|
*
|
||||||
* @param {string} motionText
|
* @param {LineNumberedString} motionText
|
||||||
* @param {LineRange} lineRange
|
* @param {LineRange} lineRange
|
||||||
* @param {boolean} lineNumbers - weather to add line numbers to the returned HTML string
|
* @param {boolean} lineNumbers - weather to add line numbers to the returned HTML string
|
||||||
* @param {number} lineLength
|
* @param {number} lineLength
|
||||||
* @param {number|null} highlightedLine
|
* @param {number|null} highlightedLine
|
||||||
*/
|
*/
|
||||||
public extractMotionLineRange(
|
public extractMotionLineRange(
|
||||||
motionText: string,
|
motionText: LineNumberedString,
|
||||||
lineRange: LineRange,
|
lineRange: LineRange,
|
||||||
lineNumbers: boolean,
|
lineNumbers: boolean,
|
||||||
lineLength: number,
|
lineLength: number,
|
||||||
highlightedLine: number
|
highlightedLine: number
|
||||||
): string {
|
): string {
|
||||||
const origHtml = this.lineNumberingService.insertLineNumbers(motionText, lineLength, highlightedLine);
|
const extracted = this.extractRangeByLineNumbers(motionText, lineRange.from, lineRange.to);
|
||||||
const extracted = this.extractRangeByLineNumbers(origHtml, lineRange.from, lineRange.to);
|
|
||||||
let html =
|
let html =
|
||||||
extracted.outerContextStart +
|
extracted.outerContextStart +
|
||||||
extracted.innerContextStart +
|
extracted.innerContextStart +
|
||||||
|
@ -1,18 +1,35 @@
|
|||||||
import { inject, TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { E2EImportsModule } from 'e2e-imports.module';
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
import { DurationService } from './duration.service';
|
import { DurationService } from './duration.service';
|
||||||
|
|
||||||
describe('DurationService', () => {
|
describe('DurationService', () => {
|
||||||
beforeEach(() =>
|
let service: DurationService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [E2EImportsModule],
|
imports: [E2EImportsModule],
|
||||||
providers: [DurationService]
|
providers: [DurationService]
|
||||||
})
|
}),
|
||||||
);
|
(service = TestBed.inject(DurationService));
|
||||||
|
});
|
||||||
|
|
||||||
it('should be created', inject([DurationService], (service: DurationService) => {
|
it('should be created', () => {
|
||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
}));
|
});
|
||||||
|
|
||||||
|
it('should return a valid duration', () => {
|
||||||
|
expect(service.durationToString(1, 'm')).toBe('0:01 m');
|
||||||
|
expect(service.durationToString(23, 'm')).toBe('0:23 m');
|
||||||
|
expect(service.durationToString(60, 'm')).toBe('1:00 m');
|
||||||
|
expect(service.durationToString(65, 'm')).toBe('1:05 m');
|
||||||
|
expect(service.durationToString(0, 'm')).toBe('0:00 m');
|
||||||
|
expect(service.durationToString(-23, 'm')).toBe('-0:23 m');
|
||||||
|
expect(service.durationToString(-65, 'm')).toBe('-1:05 m');
|
||||||
|
expect(service.durationToString(null, null)).toBe('');
|
||||||
|
expect(service.durationToString(NaN, 'h')).toBe('');
|
||||||
|
expect(service.durationToString(Infinity, 'h')).toBe('');
|
||||||
|
expect(service.durationToString(-Infinity, 'h')).toBe('');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -62,6 +62,24 @@ export class DurationService {
|
|||||||
return time;
|
return time;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates a given time to a readable string, that contains hours, minutes and seconds.
|
||||||
|
*
|
||||||
|
* @param duration The time as number (in seconds).
|
||||||
|
*
|
||||||
|
* @returns A readable time-string.
|
||||||
|
*/
|
||||||
|
public durationToStringWithHours(duration: number): string {
|
||||||
|
const hours = Math.floor(duration / 3600);
|
||||||
|
const minutes = `0${Math.floor((duration % 3600) / 60)}`.slice(-2);
|
||||||
|
const seconds = `0${Math.floor(duration % 60)}`.slice(-2);
|
||||||
|
if (!isNaN(+minutes) && !isNaN(+seconds)) {
|
||||||
|
return `${hours}:${minutes}:${seconds} h`;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a duration number (given in minutes or seconds)
|
* Converts a duration number (given in minutes or seconds)
|
||||||
*
|
*
|
||||||
@ -70,10 +88,12 @@ export class DurationService {
|
|||||||
* @returns a more human readable time representation
|
* @returns a more human readable time representation
|
||||||
*/
|
*/
|
||||||
public durationToString(duration: number, suffix: 'h' | 'm'): string {
|
public durationToString(duration: number, suffix: 'h' | 'm'): string {
|
||||||
const major = Math.floor(duration / 60);
|
const negative = duration < 0;
|
||||||
const minor = `0${duration % 60}`.slice(-2);
|
const major = negative ? Math.ceil(duration / 60) : Math.floor(duration / 60);
|
||||||
if (!isNaN(+major) && !isNaN(+minor)) {
|
const minor = `0${Math.abs(duration) % 60}`.slice(-2);
|
||||||
return `${major}:${minor} ${suffix}`;
|
if (!isNaN(+major) && !isNaN(+minor) && suffix) {
|
||||||
|
// converting the number '-0' to string results in '0', depending on the browser.
|
||||||
|
return `${major === 0 && negative ? '-' + Math.abs(major) : major}:${minor} ${suffix}`;
|
||||||
} else {
|
} else {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,11 @@ import { Injectable } from '@angular/core';
|
|||||||
const ELEMENT_NODE = 1;
|
const ELEMENT_NODE = 1;
|
||||||
const TEXT_NODE = 3;
|
const TEXT_NODE = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper to indicate that certain functions expect the provided HTML strings to contain line numbers
|
||||||
|
*/
|
||||||
|
export type LineNumberedString = string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specifies a point within a HTML Text Node where a line break might be possible, if the following word
|
* Specifies a point within a HTML Text Node where a line break might be possible, if the following word
|
||||||
* exceeds the maximum line length.
|
* exceeds the maximum line length.
|
||||||
@ -29,7 +34,8 @@ export interface LineNumberRange {
|
|||||||
/**
|
/**
|
||||||
* The end line number.
|
* The end line number.
|
||||||
* HINT: As this object is usually referring to actual line numbers, not lines,
|
* HINT: As this object is usually referring to actual line numbers, not lines,
|
||||||
* the line starting by `to` is not included in the extracted content anymore, only the text between `from` and `to`.
|
* the line starting by `to` is not included in the extracted content anymore, only the text between
|
||||||
|
* `from` and `to`.
|
||||||
*/
|
*/
|
||||||
to: number;
|
to: number;
|
||||||
}
|
}
|
||||||
@ -67,7 +73,9 @@ interface SectionHeading {
|
|||||||
*
|
*
|
||||||
* Removing line numbers from a line-numbered string:
|
* Removing line numbers from a line-numbered string:
|
||||||
* ```ts
|
* ```ts
|
||||||
* const lineNumberedHtml = '<p><span class="os-line-number line-number-1" data-line-number="1" contenteditable="false"> </span>Lorem ipsum dolorsit amet</p>';
|
* const lineNumberedHtml =
|
||||||
|
* '<p><span class="os-line-number line-number-1" data-line-number="1" contenteditable="false"> </span>
|
||||||
|
* Lorem ipsum dolorsit amet</p>';
|
||||||
* const originalHtml = this.lineNumbering.stripLineNumbers(inHtml);
|
* const originalHtml = this.lineNumbering.stripLineNumbers(inHtml);
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
@ -118,7 +126,8 @@ export class LinenumberingService {
|
|||||||
// The line number counter
|
// The line number counter
|
||||||
private currentLineNumber: number = null;
|
private currentLineNumber: number = null;
|
||||||
|
|
||||||
// Indicates that we just entered a block element and we want to add a line number without line break at the beginning.
|
// Indicates that we just entered a block element and we want to add a line number without line break
|
||||||
|
// at the beginning.
|
||||||
private prependLineNumberToFirstText = false;
|
private prependLineNumberToFirstText = false;
|
||||||
|
|
||||||
// A workaround to prevent double line numbers
|
// A workaround to prevent double line numbers
|
||||||
@ -368,22 +377,27 @@ export class LinenumberingService {
|
|||||||
* @returns {LineNumberRange}
|
* @returns {LineNumberRange}
|
||||||
*/
|
*/
|
||||||
public getLineNumberRange(html: string): LineNumberRange {
|
public getLineNumberRange(html: string): LineNumberRange {
|
||||||
const fragment = this.htmlToFragment(html);
|
const cacheKey = this.djb2hash(html);
|
||||||
const range = {
|
let range = this.lineNumberCache.get(cacheKey);
|
||||||
from: null,
|
if (!range) {
|
||||||
to: null
|
const fragment = this.htmlToFragment(html);
|
||||||
};
|
range = {
|
||||||
const lineNumbers = fragment.querySelectorAll('.os-line-number');
|
from: null,
|
||||||
for (let i = 0; i < lineNumbers.length; i++) {
|
to: null
|
||||||
const node = lineNumbers.item(i);
|
};
|
||||||
const number = parseInt(node.getAttribute('data-line-number'), 10);
|
const lineNumbers = fragment.querySelectorAll('.os-line-number');
|
||||||
if (range.from === null || number < range.from) {
|
for (let i = 0; i < lineNumbers.length; i++) {
|
||||||
range.from = number;
|
const node = lineNumbers.item(i);
|
||||||
}
|
const number = parseInt(node.getAttribute('data-line-number'), 10);
|
||||||
if (range.to === null || number + 1 > range.to) {
|
if (range.from === null || number < range.from) {
|
||||||
range.to = number + 1;
|
range.from = number;
|
||||||
|
}
|
||||||
|
if (range.to === null || number + 1 > range.to) {
|
||||||
|
range.to = number + 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.lineNumberCache.put(cacheKey, range);
|
||||||
return range;
|
return range;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -473,10 +487,17 @@ export class LinenumberingService {
|
|||||||
* @return {string[]}
|
* @return {string[]}
|
||||||
*/
|
*/
|
||||||
public splitToParagraphs(html: string): string[] {
|
public splitToParagraphs(html: string): string[] {
|
||||||
const fragment = this.htmlToFragment(html);
|
const cacheKey = this.djb2hash(html);
|
||||||
return this.splitNodeToParagraphs(fragment).map((node: Element): string => {
|
let cachedParagraphs = this.lineNumberCache.get(cacheKey);
|
||||||
return node.outerHTML;
|
if (!cachedParagraphs) {
|
||||||
});
|
const fragment = this.htmlToFragment(html);
|
||||||
|
cachedParagraphs = this.splitNodeToParagraphs(fragment).map((node: Element): string => {
|
||||||
|
return node.outerHTML;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.lineNumberCache.put(cacheKey, cachedParagraphs);
|
||||||
|
}
|
||||||
|
return cachedParagraphs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -890,7 +911,7 @@ export class LinenumberingService {
|
|||||||
highlight?: number,
|
highlight?: number,
|
||||||
callback?: () => void,
|
callback?: () => void,
|
||||||
firstLine?: number
|
firstLine?: number
|
||||||
): string {
|
): LineNumberedString {
|
||||||
let newHtml, newRoot;
|
let newHtml, newRoot;
|
||||||
|
|
||||||
if (highlight > 0) {
|
if (highlight > 0) {
|
||||||
|
@ -18,7 +18,7 @@ interface ImageConfigObject {
|
|||||||
/**
|
/**
|
||||||
* The structure of a font config
|
* The structure of a font config
|
||||||
*/
|
*/
|
||||||
interface FontConfigObject {
|
export interface FontConfigObject {
|
||||||
display_name: string;
|
display_name: string;
|
||||||
default: string;
|
default: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
@ -12,7 +12,7 @@ describe('OverlayService', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
const service: OverlayService = TestBed.get(OverlayService);
|
const service: OverlayService = TestBed.inject(OverlayService);
|
||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { MatDialog, MatDialogRef } from '@angular/material';
|
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
|
||||||
import { Observable, Subject } from 'rxjs';
|
import { Observable, Subject } from 'rxjs';
|
||||||
import { distinctUntilChanged } from 'rxjs/operators';
|
import { distinctUntilChanged } from 'rxjs/operators';
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
import { inject, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { E2EImportsModule } from 'e2e-imports.module';
|
|
||||||
|
|
||||||
import { PollService } from './poll.service';
|
|
||||||
|
|
||||||
describe('PollService', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [E2EImportsModule],
|
|
||||||
providers: [PollService]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be created', inject([PollService], (service: PollService) => {
|
|
||||||
expect(service).toBeTruthy();
|
|
||||||
}));
|
|
||||||
});
|
|
@ -1,216 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
import { _ } from 'app/core/translate/translation-marker';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The possible keys of a poll object that represent numbers.
|
|
||||||
* TODO Should be 'key of MotionPoll|AssinmentPoll if type of key is number'
|
|
||||||
*/
|
|
||||||
export type CalculablePollKey =
|
|
||||||
| 'votesvalid'
|
|
||||||
| 'votesinvalid'
|
|
||||||
| 'votescast'
|
|
||||||
| 'yes'
|
|
||||||
| 'no'
|
|
||||||
| 'abstain'
|
|
||||||
| 'votesno'
|
|
||||||
| 'votesabstain';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: may be obsolete if the server switches to lower case only
|
|
||||||
* (lower case variants are already in CalculablePollKey)
|
|
||||||
*/
|
|
||||||
export type PollVoteValue = 'Yes' | 'No' | 'Abstain' | 'Votes';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface representing possible majority calculation methods. The implementing
|
|
||||||
* calc function should return an integer number that must be reached for the
|
|
||||||
* option to successfully fulfill the quorum, or null if disabled
|
|
||||||
*/
|
|
||||||
export interface MajorityMethod {
|
|
||||||
value: string;
|
|
||||||
display_name: string;
|
|
||||||
calc: (base: number) => number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to round up the passed value of a poll.
|
|
||||||
*
|
|
||||||
* @param value The calculated value of 100%-base.
|
|
||||||
* @param addOne Flag, if the result should be increased by 1.
|
|
||||||
*
|
|
||||||
* @returns The necessary value to get the majority.
|
|
||||||
*/
|
|
||||||
export const calcMajority = (value: number, addOne: boolean = false) => {
|
|
||||||
return Math.ceil(value) + (addOne ? 1 : 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of available majority methods, used in motion and assignment polls
|
|
||||||
*/
|
|
||||||
export const PollMajorityMethod: MajorityMethod[] = [
|
|
||||||
{
|
|
||||||
value: 'simple_majority',
|
|
||||||
display_name: 'Simple majority',
|
|
||||||
calc: base => calcMajority(base * 0.5, true)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'two-thirds_majority',
|
|
||||||
display_name: 'Two-thirds majority',
|
|
||||||
calc: base => calcMajority((base / 3) * 2)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'three-quarters_majority',
|
|
||||||
display_name: 'Three-quarters majority',
|
|
||||||
calc: base => calcMajority((base / 4) * 3)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'disabled',
|
|
||||||
display_name: 'Disabled',
|
|
||||||
calc: a => null
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared service class for polls. Used by child classes {@link MotionPollService}
|
|
||||||
* and {@link AssignmentPollService}
|
|
||||||
*/
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export abstract class PollService {
|
|
||||||
/**
|
|
||||||
* The chosen and currently used base for percentage calculations. Is
|
|
||||||
* supposed to be set by a config service
|
|
||||||
*/
|
|
||||||
public percentBase: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The default majority method (to be set set per config).
|
|
||||||
*/
|
|
||||||
public defaultMajorityMethod: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The majority method currently in use
|
|
||||||
*/
|
|
||||||
public majorityMethod: MajorityMethod;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An array of value - label pairs for special value signifiers.
|
|
||||||
* TODO: Should be given by the server, and editable. For now they are hard
|
|
||||||
* coded
|
|
||||||
*/
|
|
||||||
private _specialPollVotes: [number, string][] = [
|
|
||||||
[-1, 'majority'],
|
|
||||||
[-2, 'undocumented']
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* getter for the special vote values
|
|
||||||
*
|
|
||||||
* @returns an array of special (non-positive) numbers used in polls and
|
|
||||||
* their descriptive strings
|
|
||||||
*/
|
|
||||||
public get specialPollVotes(): [number, string][] {
|
|
||||||
return this._specialPollVotes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* empty constructor
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public constructor() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets an icon for a Poll Key
|
|
||||||
*
|
|
||||||
* @param key yes, no, abstain or something like that
|
|
||||||
* @returns a string for material-icons to represent the icon for
|
|
||||||
* this key(e.g. yes: positive sign, no: negative sign)
|
|
||||||
*/
|
|
||||||
public getIcon(key: CalculablePollKey): string {
|
|
||||||
switch (key) {
|
|
||||||
case 'yes':
|
|
||||||
return 'thumb_up';
|
|
||||||
case 'no':
|
|
||||||
case 'votesno':
|
|
||||||
return 'thumb_down';
|
|
||||||
case 'abstain':
|
|
||||||
case 'votesabstain':
|
|
||||||
return 'not_interested';
|
|
||||||
// TODO case 'votescast':
|
|
||||||
// sum
|
|
||||||
case 'votesvalid':
|
|
||||||
return 'check';
|
|
||||||
case 'votesinvalid':
|
|
||||||
return 'cancel';
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a label for a poll Key
|
|
||||||
*
|
|
||||||
* @param key yes, no, abstain or something like that
|
|
||||||
* @returns A short descriptive name for the poll keys
|
|
||||||
*/
|
|
||||||
public getLabel(key: CalculablePollKey | PollVoteValue): string {
|
|
||||||
switch (key.toLowerCase()) {
|
|
||||||
case 'yes':
|
|
||||||
return 'Yes';
|
|
||||||
case 'no':
|
|
||||||
case 'votesno':
|
|
||||||
return 'No';
|
|
||||||
case 'abstain':
|
|
||||||
case 'votesabstain':
|
|
||||||
return 'Abstain';
|
|
||||||
case 'votescast':
|
|
||||||
return _('Total votes cast');
|
|
||||||
case 'votesvalid':
|
|
||||||
return _('Valid votes');
|
|
||||||
case 'votesinvalid':
|
|
||||||
return _('Invalid votes');
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* retrieve special labels for a poll value
|
|
||||||
* {@link specialPollVotes}. Positive values will return as string
|
|
||||||
* representation of themselves
|
|
||||||
*
|
|
||||||
* @param value check value for special numbers
|
|
||||||
* @returns the label for a non-positive value, according to
|
|
||||||
*/
|
|
||||||
public getSpecialLabel(value: number): string {
|
|
||||||
if (value >= 0) {
|
|
||||||
return value.toString();
|
|
||||||
// TODO: toLocaleString(lang); but translateService is not usable here, thus lang is not well defined
|
|
||||||
}
|
|
||||||
const vote = this.specialPollVotes.find(special => special[0] === value);
|
|
||||||
return vote ? vote[1] : 'Undocumented special (negative) value';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the progress bar class for a decision key
|
|
||||||
*
|
|
||||||
* @param key a calculable poll key (like yes or no)
|
|
||||||
* @returns a css class designing a progress bar in a color, or an empty string
|
|
||||||
*/
|
|
||||||
public getProgressBarColor(key: CalculablePollKey | PollVoteValue): string {
|
|
||||||
switch (key.toLowerCase()) {
|
|
||||||
case 'yes':
|
|
||||||
return 'progress-green';
|
|
||||||
case 'no':
|
|
||||||
return 'progress-red';
|
|
||||||
case 'abstain':
|
|
||||||
return 'progress-yellow';
|
|
||||||
case 'votes':
|
|
||||||
return 'progress-green';
|
|
||||||
default:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,7 +6,7 @@ describe('ProgressService', () => {
|
|||||||
beforeEach(() => TestBed.configureTestingModule({}));
|
beforeEach(() => TestBed.configureTestingModule({}));
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
const service: ProgressService = TestBed.get(ProgressService);
|
const service: ProgressService = TestBed.inject(ProgressService);
|
||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -12,7 +12,7 @@ export class ThemeService {
|
|||||||
/**
|
/**
|
||||||
* Constant, that describes the default theme class.
|
* Constant, that describes the default theme class.
|
||||||
*/
|
*/
|
||||||
public static DEFAULT_THEME = 'openslides-theme';
|
public static DEFAULT_THEME = 'openslides-default-light-theme';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constant path of the logo with dark colors for bright themes.
|
* Constant path of the logo with dark colors for bright themes.
|
||||||
@ -54,7 +54,7 @@ export class ThemeService {
|
|||||||
this.currentTheme = theme;
|
this.currentTheme = theme;
|
||||||
|
|
||||||
const classList = document.getElementsByTagName('body')[0].classList; // Get the classlist of the body.
|
const classList = document.getElementsByTagName('body')[0].classList; // Get the classlist of the body.
|
||||||
const toRemove = Array.from(classList).filter((item: string) => item.includes('theme'));
|
const toRemove = Array.from(classList).filter((item: string) => item.includes('-theme'));
|
||||||
if (toRemove.length) {
|
if (toRemove.length) {
|
||||||
classList.remove(...toRemove); // Remove all old themes.
|
classList.remove(...toRemove); // Remove all old themes.
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ describe('TreeSortService', () => {
|
|||||||
|
|
||||||
// TODO testing (does not work without injecting a BaseViewComponent)
|
// TODO testing (does not work without injecting a BaseViewComponent)
|
||||||
// it('should be created', () => {
|
// it('should be created', () => {
|
||||||
// const service: TreeSortService = TestBed.get(TreeSortService);
|
// const service: TreeSortService = TestBed.inject(TreeSortService);
|
||||||
// expect(service).toBeTruthy();
|
// expect(service).toBeTruthy();
|
||||||
// });
|
// });
|
||||||
});
|
});
|
||||||
|
@ -354,7 +354,8 @@ export class TreeService {
|
|||||||
*
|
*
|
||||||
* @param item The current item from which the flat node will be created.
|
* @param item The current item from which the flat node will be created.
|
||||||
* @param level The level the flat node will be.
|
* @param level The level the flat node will be.
|
||||||
* @param additionalTag Optional: A key of the items. If this parameter is set, the nodes will have a tag for filtering them.
|
* @param additionalTag Optional: A key of the items. If this parameter is set,
|
||||||
|
* the nodes will have a tag for filtering them.
|
||||||
*
|
*
|
||||||
* @returns An array containing the parent node with all its children.
|
* @returns An array containing the parent node with all its children.
|
||||||
*/
|
*/
|
||||||
|
@ -3,8 +3,6 @@ import { SwUpdate, UpdateAvailableEvent } from '@angular/service-worker';
|
|||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import { NotifyService } from '../core-services/notify.service';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle Service Worker updates using the SwUpdate service form angular.
|
* Handle Service Worker updates using the SwUpdate service form angular.
|
||||||
*/
|
*/
|
||||||
@ -12,8 +10,6 @@ import { NotifyService } from '../core-services/notify.service';
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class UpdateService {
|
export class UpdateService {
|
||||||
private static NOTIFY_NAME = 'swCheckForUpdate';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns the updateSubscription
|
* @returns the updateSubscription
|
||||||
*/
|
*/
|
||||||
@ -28,12 +24,7 @@ export class UpdateService {
|
|||||||
* @param swUpdate Service Worker update service
|
* @param swUpdate Service Worker update service
|
||||||
* @param matSnackBar Currently to show that an update is available
|
* @param matSnackBar Currently to show that an update is available
|
||||||
*/
|
*/
|
||||||
public constructor(private swUpdate: SwUpdate, private notify: NotifyService) {
|
public constructor(private swUpdate: SwUpdate) {}
|
||||||
// Listen on requests from other users to check for updates.
|
|
||||||
this.notify.getMessageObservable(UpdateService.NOTIFY_NAME).subscribe(() => {
|
|
||||||
this.checkForUpdate();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manually applies the update if one was found
|
* Manually applies the update if one was found
|
||||||
@ -52,13 +43,4 @@ export class UpdateService {
|
|||||||
this.swUpdate.checkForUpdate();
|
this.swUpdate.checkForUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Emits a message to all clients initiating to check for updates. This method
|
|
||||||
* can only be called by users with 'users.can_manage'. This will be checked by
|
|
||||||
* the server.
|
|
||||||
*/
|
|
||||||
public initiateUpdateCheckForAllClients(): void {
|
|
||||||
this.notify.sendToAllUsers(UpdateService.NOTIFY_NAME, {});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { VotingBannerService } from './voting-banner.service';
|
||||||
|
|
||||||
|
describe('VotingBannerService', () => {
|
||||||
|
beforeEach(() =>
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
const service: VotingBannerService = TestBed.inject(VotingBannerService);
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
99
client/src/app/core/ui-services/voting-banner.service.ts
Normal file
99
client/src/app/core/ui-services/voting-banner.service.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
|
||||||
|
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
|
||||||
|
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
|
||||||
|
import { PollListObservableService } from 'app/site/polls/services/poll-list-observable.service';
|
||||||
|
import { BannerDefinition, BannerService } from './banner.service';
|
||||||
|
import { OpenSlidesStatusService } from '../core-services/openslides-status.service';
|
||||||
|
import { VotingService } from './voting.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class VotingBannerService {
|
||||||
|
private currentBanner: BannerDefinition;
|
||||||
|
|
||||||
|
private subText = _('Click here to vote!');
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
pollListObservableService: PollListObservableService,
|
||||||
|
private banner: BannerService,
|
||||||
|
private translate: TranslateService,
|
||||||
|
private OSStatus: OpenSlidesStatusService,
|
||||||
|
private votingService: VotingService
|
||||||
|
) {
|
||||||
|
pollListObservableService.getViewModelListObservable().subscribe(polls => this.checkForVotablePolls(polls));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks all polls for votable ones and displays a banner for them
|
||||||
|
* @param polls the updated poll list
|
||||||
|
*/
|
||||||
|
private checkForVotablePolls(polls: ViewBasePoll[]): void {
|
||||||
|
// display no banner if in history mode or there are no polls to vote
|
||||||
|
const pollsToVote = polls.filter(poll => this.votingService.canVote(poll) && !poll.user_has_voted);
|
||||||
|
if ((this.OSStatus.isInHistoryMode && this.currentBanner) || !pollsToVote.length) {
|
||||||
|
this.sliceBanner();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const banner =
|
||||||
|
pollsToVote.length === 1
|
||||||
|
? this.createBanner(this.getTextForPoll(pollsToVote[0]), pollsToVote[0].parentLink)
|
||||||
|
: this.createBanner(`${pollsToVote.length} ${this.translate.instant('open votes')}`, '/polls/');
|
||||||
|
this.sliceBanner(banner);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new `BannerDefinition` and returns it.
|
||||||
|
*
|
||||||
|
* @param text The text for the banner.
|
||||||
|
* @param link The link for the banner.
|
||||||
|
*
|
||||||
|
* @returns The created banner.
|
||||||
|
*/
|
||||||
|
private createBanner(text: string, link: string): BannerDefinition {
|
||||||
|
return {
|
||||||
|
text: text,
|
||||||
|
subText: this.subText,
|
||||||
|
link: link,
|
||||||
|
icon: 'how_to_vote',
|
||||||
|
largerOnMobileView: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns for a given poll a title for the banner.
|
||||||
|
*
|
||||||
|
* @param poll The given poll.
|
||||||
|
*
|
||||||
|
* @returns The title.
|
||||||
|
*/
|
||||||
|
private getTextForPoll(poll: ViewBasePoll): string {
|
||||||
|
if (poll instanceof ViewMotionPoll) {
|
||||||
|
return `${this.translate.instant('Motion')} ${poll.motion.getIdentifierOrTitle()}: ${this.translate.instant(
|
||||||
|
'Voting opened'
|
||||||
|
)}`;
|
||||||
|
} else if (poll instanceof ViewAssignmentPoll) {
|
||||||
|
return `${poll.assignment.getTitle()}: ${this.translate.instant('Ballot opened')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the current banner or replaces it, if a new one is given.
|
||||||
|
*
|
||||||
|
* @param nextBanner Optional the next banner to show.
|
||||||
|
*/
|
||||||
|
private sliceBanner(nextBanner?: BannerDefinition): void {
|
||||||
|
if (nextBanner) {
|
||||||
|
this.banner.replaceBanner(this.currentBanner, nextBanner);
|
||||||
|
} else {
|
||||||
|
this.banner.removeBanner(this.currentBanner);
|
||||||
|
}
|
||||||
|
this.currentBanner = nextBanner || null;
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user