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/*
|
||||
bower_components/*
|
||||
|
||||
# OS4-Submodules
|
||||
/openslides-*
|
||||
|
||||
# OS3+
|
||||
/server/
|
||||
|
||||
# Local user data (settings, database, media, search index, static files)
|
||||
personal_data/*
|
||||
openslides/static/*
|
||||
@ -26,6 +32,7 @@ dist/*
|
||||
debug/*
|
||||
.DS_Store
|
||||
.idea
|
||||
*.code-workspace
|
||||
|
||||
# Unit test and coverage reports
|
||||
.coverage
|
||||
@ -77,6 +84,7 @@ client/yarn.lock
|
||||
package-lock.json
|
||||
client/package-lock.json
|
||||
cypress.json
|
||||
*-version.txt
|
||||
|
||||
# System Files
|
||||
client/.DS_Store
|
||||
|
21
.travis.yml
21
.travis.yml
@ -25,7 +25,7 @@ matrix:
|
||||
|
||||
- name: "Installing npm dependencies"
|
||||
language: node_js
|
||||
node_js: "10.9"
|
||||
node_js: "12.18"
|
||||
cache:
|
||||
- directories:
|
||||
- "client/node_modules"
|
||||
@ -39,7 +39,7 @@ matrix:
|
||||
- stage: "Run tests"
|
||||
name: "Client: Testing"
|
||||
language: node_js
|
||||
node_js: "10.9"
|
||||
node_js: "12.18"
|
||||
apt:
|
||||
sources:
|
||||
- google-chrome
|
||||
@ -56,7 +56,7 @@ matrix:
|
||||
|
||||
- name: "Client: Production Build (ES5)"
|
||||
language: node_js
|
||||
node_js: "10.9"
|
||||
node_js: "12.18"
|
||||
install:
|
||||
- cd client
|
||||
- sed -i '/\"target\"/c\\"target\":\"es5\",' tsconfig.json
|
||||
@ -65,7 +65,7 @@ matrix:
|
||||
|
||||
- name: "Client: Production Build (ES2015)"
|
||||
language: node_js
|
||||
node_js: "10.9"
|
||||
node_js: "12.18"
|
||||
install:
|
||||
- cd client
|
||||
- echo "Firefox ESR" > browserslist
|
||||
@ -74,7 +74,7 @@ matrix:
|
||||
|
||||
- name: "Client: Build"
|
||||
language: node_js
|
||||
node_js: "10.9"
|
||||
node_js: "12.18"
|
||||
script:
|
||||
- cd client
|
||||
- npm run build-debug
|
||||
@ -85,7 +85,7 @@ matrix:
|
||||
- "3.6"
|
||||
script:
|
||||
- mypy openslides/ tests/
|
||||
- pytest --cov --cov-fail-under=73
|
||||
- pytest --cov --cov-fail-under=75
|
||||
|
||||
- name: "Server: Tests Python 3.7"
|
||||
language: python
|
||||
@ -96,7 +96,7 @@ matrix:
|
||||
- isort --check-only --diff --recursive openslides tests
|
||||
- black --check --diff --target-version py36 openslides tests
|
||||
- mypy openslides/ tests/
|
||||
- pytest --cov --cov-fail-under=73
|
||||
- pytest --cov --cov-fail-under=75
|
||||
|
||||
- name: "Server: Tests Python 3.8"
|
||||
language: python
|
||||
@ -107,21 +107,20 @@ matrix:
|
||||
- isort --check-only --diff --recursive openslides tests
|
||||
- black --check --diff --target-version py36 openslides tests
|
||||
- mypy openslides/ tests/
|
||||
- pytest --cov --cov-fail-under=73
|
||||
- pytest --cov --cov-fail-under=75
|
||||
|
||||
- name: "Client: Linting"
|
||||
language: node_js
|
||||
node_js: "10.9"
|
||||
node_js: "12.18"
|
||||
script:
|
||||
- cd client
|
||||
- npm run lint-check
|
||||
|
||||
- name: "Client: Code Formatting Check"
|
||||
language: node_js
|
||||
node_js: "10.9"
|
||||
node_js: "12.18"
|
||||
script:
|
||||
- cd client
|
||||
- npm list --depth=0 || cat --help
|
||||
- npm run prettify-check
|
||||
|
||||
- name: "Server: Tests Startup Routine Python 3.7"
|
||||
|
@ -4,9 +4,79 @@
|
||||
|
||||
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)
|
||||
========================
|
||||
`Milestone <https://github.com/OpenSlides/OpenSlides/milestones/3.0>`_
|
||||
`Milestone <https://github.com/OpenSlides/OpenSlides/milestones/3.1>`_
|
||||
|
||||
General:
|
||||
- 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;
|
||||
}
|
||||
location /rest {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_pass http://localhost:8000;
|
||||
}
|
||||
location /ws {
|
||||
|
@ -5,7 +5,7 @@ RUN mkdir /app
|
||||
RUN apt -y update && \
|
||||
apt -y upgrade && \
|
||||
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 npm install -g @angular/cli@latest
|
||||
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'
|
||||
|
||||
|
||||
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
|
||||
=======
|
||||
|
||||
@ -66,7 +85,7 @@ We recommend to enable all OpenSlides related logging with level `INFO` per
|
||||
default::
|
||||
|
||||
LOGGING = {
|
||||
'formatters':
|
||||
'formatters': {
|
||||
'lessnoise': {
|
||||
'format': '[{levelname}] {name} {message}',
|
||||
'style': '{',
|
||||
@ -123,3 +142,7 @@ not affect the client.
|
||||
operator is in one of these groups, the client disconnected and reconnects again.
|
||||
All requests urls (including websockets) are now prefixed with `/prioritize`, so
|
||||
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
|
||||
|
125
client/README.md
125
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:
|
||||
|
||||
- [@angular/animations@8.2.4](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/cdk-experimental@8.1.4](https://github.com/angular/components), License: MIT
|
||||
- [@angular/cdk@8.1.4](https://github.com/angular/components), License: MIT
|
||||
- [@angular/common@8.2.4](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/compiler@8.2.4](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/core@8.2.4](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/forms@8.2.4](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/material-moment-adapter@8.1.4](https://github.com/angular/components), License: MIT
|
||||
- [@angular/material@8.1.4](https://github.com/angular/components), License: MIT
|
||||
- [@angular/platform-browser-dynamic@8.2.4](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/platform-browser@8.2.4](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/pwa@0.803.2](https://github.com/angular/angular-cli), License: MIT
|
||||
- [@angular/router@8.2.4](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/service-worker@8.2.4](https://github.com/angular/angular), License: MIT
|
||||
- [@ngx-pwa/local-storage@8.2.1](https://github.com/cyrilletuzi/angular-async-local-storage), License: MIT
|
||||
- [@ngx-translate/core@11.0.1](https://github.com/ngx-translate/core), License: MIT
|
||||
- [@angular/animations@9.1.0](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/cdk-experimental@9.2.0](https://github.com/angular/components), License: MIT
|
||||
- [@angular/cdk@9.2.0](https://github.com/angular/components), License: MIT
|
||||
- [@angular/common@9.1.0](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/compiler@9.1.0](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/core@9.1.0](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/forms@9.1.0](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/material-moment-adapter@9.2.0](https://github.com/angular/components), License: MIT
|
||||
- [@angular/material@9.2.0](https://github.com/angular/components), License: MIT
|
||||
- [@angular/platform-browser-dynamic@9.1.0](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/platform-browser@9.1.0](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/router@9.1.0](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/service-worker@9.1.0](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-translate/core@12.1.2](https://github.com/ngx-translate/core), License: MIT
|
||||
- [@ngx-translate/http-loader@4.0.0](https://github.com/ngx-translate/http-loader), License: MIT
|
||||
- [@pebula/ngrid-material@1.0.0-rc.5](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.0](https://github.com/shlomiassaf/ngrid), License: MIT
|
||||
- [@tinymce/tinymce-angular@3.3.0](https://github.com/tinymce/tinymce-angular), License: Apache-2.0
|
||||
- [acorn@7.0.0](https://github.com/acornjs/acorn), License: MIT
|
||||
- [core-js@3.2.1](https://github.com/zloirock/core-js), License: MIT
|
||||
- [css-element-queries@1.2.1](https://github.com/marcj/css-element-queries), License: MIT
|
||||
- [exceljs@1.15.0](https://github.com/exceljs/exceljs), License: MIT
|
||||
- [@pebula/ngrid-material@2.0.0-rc.1](undefined), License: MIT
|
||||
- [@pebula/ngrid@2.0.0-rc.1](https://github.com/shlomiassaf/ngrid), License: MIT
|
||||
- [@pebula/utils@1.0.2](undefined), License: MIT
|
||||
- [@tinymce/tinymce-angular@3.5.0](https://github.com/tinymce/tinymce-angular), License: Apache-2.0
|
||||
- [acorn@7.1.1](https://github.com/acornjs/acorn), License: MIT
|
||||
- [chart.js@2.9.3](https://github.com/chartjs/Chart.js), License: MIT
|
||||
- [core-js@3.6.4](https://github.com/zloirock/core-js), License: MIT
|
||||
- [css-element-queries@1.2.3](https://github.com/marcj/css-element-queries), 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
|
||||
- [hammerjs@2.0.8](https://github.com/hammerjs/hammer.js), License: MIT
|
||||
- [lz4js@0.2.0](https://github.com/Benzinga/lz4js), 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
|
||||
- [ng2-pdf-viewer@5.3.4](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-mat-select-search@1.8.0](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-papaparse@4.0.2](https://github.com/alberthaff/ngx-papaparse), License: MIT
|
||||
- [pdfmake@0.1.58](https://github.com/bpampuch/pdfmake), License: MIT
|
||||
- [po2json@1.0.0-alpha](https://github.com/mikeedwards/po2json), License: GNU Library General Public License
|
||||
- [rxjs@6.5.2](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.0.14](https://github.com/tinymce/tinymce-dist), License: LGPL-2.1
|
||||
- [tslib@1.10.0](https://github.com/Microsoft/tslib), License: Apache-2.0
|
||||
- [uuid@3.3.3](https://github.com/kelektiv/node-uuid), License: MIT
|
||||
- [zone.js@0.9.1](https://github.com/angular/zone.js), License: MIT
|
||||
- [@angular-devkit/build-angular@0.803.2](https://github.com/angular/angular-cli), License: MIT
|
||||
- [@angular/cli@8.3.2](https://github.com/angular/angular-cli), License: MIT
|
||||
- [@angular/compiler-cli@8.2.4](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/language-service@8.2.4](https://github.com/angular/angular), License: MIT
|
||||
- [@biesbjerg/ngx-translate-extract@3.0.5](https://github.com/biesbjerg/ngx-translate-extract), License: MIT
|
||||
- [@compodoc/compodoc@1.1.10](https://github.com/compodoc/compodoc), License: MIT
|
||||
- [@types/jasmine@3.4.0](https://github.com/DefinitelyTyped/DefinitelyTyped), License: MIT
|
||||
- [@types/jasminewd2@2.0.6](https://github.com/DefinitelyTyped/DefinitelyTyped), License: MIT
|
||||
- [@types/node@12.7.3](https://github.com/DefinitelyTyped/DefinitelyTyped), License: MIT
|
||||
- [@types/yargs@13.0.2](https://github.com/DefinitelyTyped/DefinitelyTyped), License: MIT
|
||||
- [codelyzer@5.1.0](https://github.com/mgechev/codelyzer), License: MIT
|
||||
- [husky@3.0.4](https://github.com/typicode/husky), License: MIT
|
||||
- [jasmine-core@3.4.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
|
||||
- [ng2-charts@2.3.0](https://github.com/valor-software/ng2-charts), License: ISC
|
||||
- [ng2-pdf-viewer@6.1.2](git+https://vadimdez@github.com/VadimDez/ng2-pdf-viewer), License: MIT
|
||||
- [ngx-file-drop@8.0.8](https://github.com/georgipeltekov/ngx-file-drop), License: MIT
|
||||
- [ngx-mat-select-search@2.1.2](https://github.com/bithost-gmbh/ngx-mat-select-search), License: MIT
|
||||
- [ngx-material-timepicker@5.5.1](https://github.com/Agranom/ngx-material-timepicker), License: MIT
|
||||
- [ngx-papaparse@4.0.4](https://github.com/alberthaff/ngx-papaparse), License: MIT
|
||||
- [pdfmake@0.1.65](https://github.com/bpampuch/pdfmake), License: MIT
|
||||
- [po2json@1.0.0-beta-2](https://github.com/mikeedwards/po2json), License: LGPL-2.0-or-later
|
||||
- [rxjs@6.5.4](https://github.com/reactivex/rxjs), License: Apache-2.0
|
||||
- [tinymce@5.2.1](https://github.com/tinymce/tinymce-dist), License: LGPL-2.1
|
||||
- [tslib@1.11.1](https://github.com/Microsoft/tslib), License: Apache-2.0
|
||||
- [zone.js@0.10.3](https://github.com/angular/angular), License: MIT
|
||||
- [@angular-devkit/build-angular@0.901.0](https://github.com/angular/angular-cli), License: MIT
|
||||
- [@angular-devkit/schematics@9.1.0](https://github.com/angular/angular-cli), License: MIT
|
||||
- [@angular/cli@9.1.0](https://github.com/angular/angular-cli), License: MIT
|
||||
- [@angular/compiler-cli@9.1.0](https://github.com/angular/angular), License: MIT
|
||||
- [@angular/language-service@9.1.0](https://github.com/angular/angular), License: MIT
|
||||
- [@biesbjerg/ngx-translate-extract@6.0.3](https://github.com/biesbjerg/ngx-translate-extract), License: MIT
|
||||
- [@compodoc/compodoc@1.1.11](https://github.com/compodoc/compodoc), License: MIT
|
||||
- [@schematics/angular@9.1.0](https://github.com/angular/angular-cli), License: MIT
|
||||
- [@types/jasmine@3.5.10](https://github.com/DefinitelyTyped/DefinitelyTyped), License: MIT
|
||||
- [@types/jasminewd2@2.0.8](https://github.com/DefinitelyTyped/DefinitelyTyped), License: MIT
|
||||
- [@types/node@13.9.8](https://github.com/DefinitelyTyped/DefinitelyTyped), License: MIT
|
||||
- [@types/yargs@15.0.4](https://github.com/DefinitelyTyped/DefinitelyTyped), License: MIT
|
||||
- [codelyzer@5.2.2](https://github.com/mgechev/codelyzer), License: MIT
|
||||
- [husky@4.2.3](https://github.com/typicode/husky), License: MIT
|
||||
- [jasmine-core@3.5.0](https://github.com/jasmine/jasmine), License: MIT
|
||||
- [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-coverage-istanbul-reporter@2.1.0](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@2.0.1](https://github.com/karma-runner/karma-jasmine), License: MIT
|
||||
- [karma@4.3.0](https://github.com/karma-runner/karma), License: MIT
|
||||
- [karma-coverage-istanbul-reporter@2.1.1](https://github.com/mattlewis92/karma-coverage-istanbul-reporter), License: MIT
|
||||
- [karma-jasmine-html-reporter@1.5.3](https://github.com/dfederm/karma-jasmine-html-reporter), License: MIT
|
||||
- [karma-jasmine@3.1.1](https://github.com/karma-runner/karma-jasmine), 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-run-all@4.1.5](https://github.com/mysticatea/npm-run-all), License: MIT
|
||||
- [prettier@1.18.2](https://github.com/prettier/prettier), License: MIT
|
||||
- [protractor@5.4.2](https://github.com/angular/protractor), License: MIT
|
||||
- [prettier@2.0.2](https://github.com/prettier/prettier), License: MIT
|
||||
- [protractor@5.4.3](https://github.com/angular/protractor), License: MIT
|
||||
- [resize-observer-polyfill@1.5.1](https://github.com/que-etc/resize-observer-polyfill), License: MIT
|
||||
- [source-map-explorer@2.0.1](https://github.com/danvk/source-map-explorer), License: Apache-2.0
|
||||
- [ts-node@8.3.0](https://github.com/TypeStrong/ts-node), License: MIT
|
||||
- [tslint@5.19.0](https://github.com/palantir/tslint), License: Apache-2.0
|
||||
- [ts-node@8.8.1](https://github.com/TypeStrong/ts-node), License: MIT
|
||||
- [tslint@6.1.0](https://github.com/palantir/tslint), 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
|
||||
- [typescript@3.8.3](https://github.com/Microsoft/TypeScript), License: Apache-2.0
|
||||
|
@ -7,7 +7,7 @@
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"styleext": "scss"
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
@ -22,7 +22,7 @@
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": false,
|
||||
"aot": true,
|
||||
"assets": [
|
||||
"src/assets",
|
||||
"src/manifest.json",
|
||||
@ -43,15 +43,21 @@
|
||||
}
|
||||
],
|
||||
"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"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [{
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}],
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
@ -62,13 +68,25 @@
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"serviceWorker": true,
|
||||
"budgets": [{
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "5mb",
|
||||
"maximumError": "10mb"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb"
|
||||
}
|
||||
]
|
||||
},
|
||||
"es5": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb"
|
||||
}
|
||||
],
|
||||
"tsConfig": "./tsconfig-es5.app.json"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "OpenSlides3-Client",
|
||||
"version": "3.1.1",
|
||||
"version": "3.2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/OpenSlides/OpenSlides.git"
|
||||
@ -10,19 +10,20 @@
|
||||
"README": "https://github.com/OpenSlides/OpenSlides/blob/master/client/README.md",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"ng-high-memory": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng",
|
||||
"start": "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",
|
||||
"build": "npm run ng-high-memory -- build --prod",
|
||||
"build-debug": "npm run ng-high-memory -- build",
|
||||
"build": "ng build --prod",
|
||||
"postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points",
|
||||
"build-debug": "ng build",
|
||||
"test": "ng test",
|
||||
"test-silently": "npm run test -- --watch=false --no-progress --browsers=ChromeHeadlessNoSandbox",
|
||||
"test-live": "npm run test -- --watch=true --browsers=ChromeHeadlessNoSandbox",
|
||||
"lint-check": "ng lint",
|
||||
"lint-write": "ng lint --fix",
|
||||
"e2e": "ng e2e",
|
||||
"licenses": "node src/crawler.js",
|
||||
"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-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}\"",
|
||||
@ -31,79 +32,81 @@
|
||||
"cleanup-win": "npm run prettify-write & npm run lint-write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "~8.2.4",
|
||||
"@angular/cdk": "~8.1.4",
|
||||
"@angular/cdk-experimental": "~8.1.4",
|
||||
"@angular/common": "~8.2.4",
|
||||
"@angular/compiler": "~8.2.4",
|
||||
"@angular/core": "~8.2.4",
|
||||
"@angular/forms": "~8.2.4",
|
||||
"@angular/material": "~8.1.4",
|
||||
"@angular/material-moment-adapter": "~8.1.4",
|
||||
"@angular/platform-browser": "~8.2.4",
|
||||
"@angular/platform-browser-dynamic": "~8.2.4",
|
||||
"@angular/pwa": "^0.803.1",
|
||||
"@angular/router": "~8.2.4",
|
||||
"@angular/service-worker": "~8.2.4",
|
||||
"@ngx-pwa/local-storage": "~8.2.1",
|
||||
"@ngx-translate/core": "~11.0.1",
|
||||
"@angular/animations": "~9.1.0",
|
||||
"@angular/cdk": "~9.2.0",
|
||||
"@angular/cdk-experimental": "~9.2.0",
|
||||
"@angular/common": "~9.1.0",
|
||||
"@angular/compiler": "~9.1.0",
|
||||
"@angular/core": "~9.1.0",
|
||||
"@angular/forms": "~9.1.0",
|
||||
"@angular/material": "~9.2.0",
|
||||
"@angular/material-moment-adapter": "~9.2.0",
|
||||
"@angular/platform-browser": "~9.1.0",
|
||||
"@angular/platform-browser-dynamic": "~9.1.0",
|
||||
"@angular/router": "~9.1.0",
|
||||
"@angular/service-worker": "~9.1.0",
|
||||
"@ngx-pwa/local-storage": "~9.0.2",
|
||||
"@ngx-translate/core": "~12.1.2",
|
||||
"@ngx-translate/http-loader": "^4.0.0",
|
||||
"@pebula/ngrid": "1.0.0-rc.9",
|
||||
"@pebula/ngrid-material": "1.0.0-rc.9",
|
||||
"@pebula/utils": "1.0.0",
|
||||
"@tinymce/tinymce-angular": "^3.2.0",
|
||||
"acorn": "^7.0.0",
|
||||
"core-js": "^3.2.1",
|
||||
"css-element-queries": "^1.2.1",
|
||||
"@pebula/ngrid": "2.0.0-rc.1",
|
||||
"@pebula/ngrid-material": "2.0.0-rc.1",
|
||||
"@pebula/utils": "1.0.2",
|
||||
"@tinymce/tinymce-angular": "^3.6.0",
|
||||
"@videojs/http-streaming": "^1.13.3",
|
||||
"acorn": "^7.1.0",
|
||||
"chart.js": "^2.9.2",
|
||||
"core-js": "^3.6.4",
|
||||
"css-element-queries": "^1.2.3",
|
||||
"exceljs": "1.15.0",
|
||||
"file-saver": "^2.0.2",
|
||||
"hammerjs": "^2.0.8",
|
||||
"lz4js": "^0.2.0",
|
||||
"material-icon-font": "git+https://github.com/petergng/materialIconFont.git",
|
||||
"moment": "^2.24.0",
|
||||
"ng2-pdf-viewer": "^5.3.4",
|
||||
"ngx-file-drop": "~8.0.7",
|
||||
"ngx-mat-select-search": "^1.8.0",
|
||||
"ngx-material-timepicker": "^4.0.2",
|
||||
"ng2-charts": "^2.3.0",
|
||||
"ng2-pdf-viewer": "^6.1.2",
|
||||
"ngx-device-detector": "^1.4.4",
|
||||
"ngx-file-drop": "^9.0.1",
|
||||
"ngx-mat-select-search": "^2.1.2",
|
||||
"ngx-material-timepicker": "^5.5.1",
|
||||
"ngx-papaparse": "^4.0.2",
|
||||
"pdfmake": "^0.1.58",
|
||||
"po2json": "^1.0.0-alpha",
|
||||
"rxjs": "^6.5.2",
|
||||
"tinymce": "^5.0.14",
|
||||
"pdfmake": "^0.1.63",
|
||||
"po2json": "^1.0.0-beta-2",
|
||||
"rxjs": "^6.5.4",
|
||||
"tinymce": "5.2.2",
|
||||
"tslib": "^1.10.0",
|
||||
"uuid": "^3.3.2",
|
||||
"zone.js": "~0.9.1"
|
||||
"video.js": "^7.7.6",
|
||||
"zone.js": "~0.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.803.2",
|
||||
"@angular/cli": "~8.3.2",
|
||||
"@angular/compiler-cli": "~8.2.4",
|
||||
"@angular/language-service": "~8.2.4",
|
||||
"@biesbjerg/ngx-translate-extract": "^3.0.5",
|
||||
"@angular-devkit/build-angular": "~0.901.9",
|
||||
"@angular-devkit/schematics": "^9.0.6",
|
||||
"@angular/cli": "~9.1.0",
|
||||
"@angular/compiler-cli": "~9.1.0",
|
||||
"@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",
|
||||
"@schematics/angular": "^9.0.6",
|
||||
"@types/jasmine": "^3.3.9",
|
||||
"@types/jasminewd2": "^2.0.6",
|
||||
"@types/node": "~12.7.2",
|
||||
"@types/yargs": "^13.0.0",
|
||||
"codelyzer": "^5.0.1",
|
||||
"husky": "^3.0.4",
|
||||
"jasmine-core": "~3.4.0",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "^4.1.0",
|
||||
"@types/node": "^13.9.8",
|
||||
"@types/yargs": "^15.0.4",
|
||||
"codelyzer": "^5.1.2",
|
||||
"husky": "^4.2.3",
|
||||
"jasmine-core": "~3.5.0",
|
||||
"jasmine-spec-reporter": "~5.0.1",
|
||||
"karma": "^4.4.1",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "^2.0.5",
|
||||
"karma-jasmine": "~2.0.1",
|
||||
"karma-jasmine": "~3.1.1",
|
||||
"karma-jasmine-html-reporter": "^1.4.0",
|
||||
"npm-license-crawler": "^0.2.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^1.19.1",
|
||||
"protractor": "^5.4.2",
|
||||
"prettier": "^2.0.5",
|
||||
"protractor": "^5.4.3",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"source-map-explorer": "^2.0.1",
|
||||
"ts-node": "~8.3.0",
|
||||
"tslint": "~5.19.0",
|
||||
"ts-node": "~8.8.1",
|
||||
"tslint": "~6.1.0",
|
||||
"tsutils": "3.17.1",
|
||||
"typescript": "~3.5.3",
|
||||
"webpack-bundle-analyzer": "^3.3.2"
|
||||
"typescript": "~3.8.3"
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import { LoginPrivacyPolicyComponent } from './site/login/components/login-priva
|
||||
import { LoginWrapperComponent } from './site/login/components/login-wrapper/login-wrapper.component';
|
||||
import { ResetPasswordConfirmComponent } from './site/login/components/reset-password-confirm/reset-password-confirm.component';
|
||||
import { ResetPasswordComponent } from './site/login/components/reset-password/reset-password.component';
|
||||
import { UnsupportedBrowserComponent } from './site/login/components/unsupported-browser/unsupported-browser.component';
|
||||
|
||||
/**
|
||||
* Global app routing
|
||||
@ -20,7 +21,8 @@ const routes: Routes = [
|
||||
{ path: 'reset-password', component: ResetPasswordComponent },
|
||||
{ path: 'reset-password-confirm', component: ResetPasswordConfirmComponent },
|
||||
{ path: 'legalnotice', component: LoginLegalNoticeComponent },
|
||||
{ path: 'privacypolicy', component: LoginPrivacyPolicyComponent }
|
||||
{ path: 'privacypolicy', component: LoginPrivacyPolicyComponent },
|
||||
{ path: 'unsupported-browser', component: UnsupportedBrowserComponent }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -1,3 +1,4 @@
|
||||
.content {
|
||||
flex: 1;
|
||||
height: 100vh;
|
||||
}
|
||||
|
@ -14,8 +14,8 @@ describe('AppComponent', () => {
|
||||
imports: [E2EImportsModule]
|
||||
}).compileComponents();
|
||||
|
||||
servertimeService = TestBed.get(ServertimeService);
|
||||
translate = TestBed.get(TranslateService);
|
||||
servertimeService = TestBed.inject(ServertimeService);
|
||||
translate = TestBed.inject(TranslateService);
|
||||
spyOn(servertimeService, 'startScheduler').and.stub();
|
||||
spyOn(translate, 'addLangs').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 { ServertimeService } from './core/core-services/servertime.service';
|
||||
import { ThemeService } from './core/ui-services/theme.service';
|
||||
import { VotingBannerService } from './core/ui-services/voting-banner.service';
|
||||
|
||||
declare global {
|
||||
/**
|
||||
@ -25,6 +26,12 @@ declare global {
|
||||
*/
|
||||
interface Array<T> {
|
||||
flatMap(o: any): any[];
|
||||
intersect(a: T[]): T[];
|
||||
mapToObject(f: (item: T) => { [key: string]: any }): { [key: string]: any };
|
||||
}
|
||||
|
||||
interface Set<T> {
|
||||
equals(other: Set<T>): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -79,7 +86,8 @@ export class AppComponent {
|
||||
dataStoreUpgradeService: DataStoreUpgradeService, // to start it.
|
||||
prioritizeService: PrioritizeService,
|
||||
pingService: PingService,
|
||||
routingState: RoutingStateService
|
||||
routingState: RoutingStateService,
|
||||
votingBannerService: VotingBannerService // needed for initialisation
|
||||
) {
|
||||
// manually add the supported languages
|
||||
translate.addLangs(['en', 'de', 'cs', 'ru']);
|
||||
@ -91,8 +99,8 @@ export class AppComponent {
|
||||
translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en');
|
||||
|
||||
// change default JS functions
|
||||
this.overloadArrayToString();
|
||||
this.overloadFlatMap();
|
||||
this.overloadArrayFunctions();
|
||||
this.overloadSetFunctions();
|
||||
this.overloadModulo();
|
||||
|
||||
// Wait until the App reaches a stable state.
|
||||
@ -106,17 +114,9 @@ export class AppComponent {
|
||||
.subscribe(() => servertimeService.startScheduler());
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to alter the normal Array.toString - function
|
||||
*
|
||||
* Will add a whitespace after a comma and shorten the output to
|
||||
* three strings.
|
||||
*
|
||||
* 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 {
|
||||
private overloadArrayFunctions(): void {
|
||||
Object.defineProperty(Array.prototype, 'toString', {
|
||||
value: function (): string {
|
||||
let string = '';
|
||||
const iterations = Math.min(this.length, 3);
|
||||
|
||||
@ -132,19 +132,66 @@ export class AppComponent {
|
||||
}
|
||||
}
|
||||
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.
|
||||
* TODO: Remove once flatMap made its way into official JS/TS (ES 2019?)
|
||||
* Adds some functions to Set.
|
||||
*/
|
||||
private overloadFlatMap(): void {
|
||||
const concat = (x: any, y: any) => x.concat(y);
|
||||
const flatMap = (f: any, xs: any) => xs.map(f).reduce(concat, []);
|
||||
Array.prototype.flatMap = function(f: any): any[] {
|
||||
return flatMap(f, this);
|
||||
};
|
||||
private overloadSetFunctions(): void {
|
||||
Object.defineProperty(Set.prototype, 'equals', {
|
||||
value: function <T>(other: Set<T>): boolean {
|
||||
const difference = new Set(this);
|
||||
for (const elem of other) {
|
||||
if (difference.has(elem)) {
|
||||
difference.delete(elem);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !difference.size;
|
||||
},
|
||||
enumerable: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -152,8 +199,11 @@ export class AppComponent {
|
||||
* TODO: Remove this, if the remainder operation is changed to modulo.
|
||||
*/
|
||||
private overloadModulo(): void {
|
||||
Number.prototype.modulo = function(n: number): number {
|
||||
Object.defineProperty(Number.prototype, 'modulo', {
|
||||
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 { ServiceWorkerModule } from '@angular/service-worker';
|
||||
|
||||
import { StorageModule } from '@ngx-pwa/local-storage';
|
||||
|
||||
import { AppLoadService } from './core/core-services/app-load.service';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
@ -39,7 +41,8 @@ export function AppLoaderFactory(appLoadService: AppLoadService): () => Promise<
|
||||
CoreModule,
|
||||
LoginModule,
|
||||
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 }],
|
||||
bootstrap: [AppComponent]
|
||||
|
@ -2,6 +2,8 @@ import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { Permission } from './core/core-services/operator.service';
|
||||
|
||||
/**
|
||||
* Provides functionalities that will be used by most components
|
||||
* currently able to set the title with the suffix ' - OpenSlides'
|
||||
@ -10,6 +12,11 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
* Components in the 'Side'- or 'projector' Folder are BaseComponents
|
||||
*/
|
||||
export abstract class BaseComponent {
|
||||
/**
|
||||
* To check permissions in templates using permission.[...]
|
||||
*/
|
||||
public permission = Permission;
|
||||
|
||||
/**
|
||||
* To manipulate the browser title bar, adds the Suffix "OpenSlides"
|
||||
*
|
||||
@ -58,7 +65,9 @@ export abstract class BaseComponent {
|
||||
mobile: {
|
||||
theme: 'mobile',
|
||||
plugins: ['autosave', 'lists', 'autolink']
|
||||
}
|
||||
},
|
||||
relative_urls: false,
|
||||
remove_script_host: true
|
||||
};
|
||||
|
||||
public constructor(protected titleService: Title, protected translate: TranslateService) {
|
||||
|
@ -68,15 +68,10 @@ export class AppLoadService {
|
||||
let repository: BaseRepository<any, any, any> = null;
|
||||
repository = this.injector.get(entry.repository);
|
||||
repositories.push(repository);
|
||||
this.modelMapper.registerCollectionElement(
|
||||
entry.collectionString,
|
||||
entry.model,
|
||||
entry.viewModel,
|
||||
repository
|
||||
);
|
||||
this.modelMapper.registerCollectionElement(entry.model, entry.viewModel, repository);
|
||||
if (this.isSearchableModelEntry(entry)) {
|
||||
this.searchService.registerModel(
|
||||
entry.collectionString,
|
||||
entry.model.COLLECTIONSTRING,
|
||||
repository,
|
||||
entry.searchOrder,
|
||||
entry.openInNewTab
|
||||
@ -104,11 +99,11 @@ export class AppLoadService {
|
||||
private isSearchableModelEntry(entry: ModelEntry | SearchableModelEntry): entry is SearchableModelEntry {
|
||||
if ((<SearchableModelEntry>entry).searchOrder !== undefined) {
|
||||
// We need to double check, because Typescipt cannot check contructors. If typescript could differentiate
|
||||
// between (ModelConstructor<BaseModel>) and (new (...args: any[]) => (BaseModel & Searchable)), we would not have
|
||||
// to check if the result of the contructor (the model instance) is really a searchable.
|
||||
// between (ModelConstructor<BaseModel>) and (new (...args: any[]) => (BaseModel & Searchable)),
|
||||
// we would not have to check if the result of the contructor (the model instance) is really a searchable.
|
||||
if (!isSearchable(new entry.viewModel())) {
|
||||
throw Error(
|
||||
`Wrong configuration for ${entry.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;
|
||||
|
@ -3,7 +3,7 @@ import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router } from '@
|
||||
|
||||
import { FallbackRoutesService } from './fallback-routes.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.
|
||||
@ -36,7 +36,7 @@ export class AuthGuard implements CanActivate, CanActivateChild {
|
||||
* @param route the route the user wants to navigate to
|
||||
*/
|
||||
public canActivate(route: ActivatedRouteSnapshot): boolean {
|
||||
const basePerm: string | string[] = route.data.basePerm;
|
||||
const basePerm: Permission | Permission[] = route.data.basePerm;
|
||||
|
||||
if (!basePerm) {
|
||||
return true;
|
||||
|
@ -8,6 +8,7 @@ import { DEFAULT_AUTH_TYPE, UserAuthType } from 'app/shared/models/users/user';
|
||||
import { DataStoreService } from './data-store.service';
|
||||
import { HttpService } from './http.service';
|
||||
import { OpenSlidesService } from './openslides.service';
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
/**
|
||||
* Authenticates an OpenSlides user with username and password
|
||||
@ -29,7 +30,8 @@ export class AuthService {
|
||||
private operator: OperatorService,
|
||||
private OpenSlides: OpenSlidesService,
|
||||
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.redirectUser(response.user_id);
|
||||
} else if (authType === 'saml') {
|
||||
await this.operator.clearWhoAmIFromStorage(); // This is important:
|
||||
// Then returning to the page, we do not want to have anything cached so a
|
||||
// fresh whoami is executed.
|
||||
window.location.href = environment.urlPrefix + '/saml/?sso'; // Bye
|
||||
} else {
|
||||
throw new Error(`Unsupported auth type "${authType}"`);
|
||||
@ -67,7 +72,7 @@ export class AuthService {
|
||||
* if it wasn't done before.
|
||||
*/
|
||||
public async redirectUser(userId: number): Promise<void> {
|
||||
if (!this.OpenSlides.booted) {
|
||||
if (!this.OpenSlides.isBooted) {
|
||||
await this.OpenSlides.afterLoginBootup(userId);
|
||||
}
|
||||
|
||||
@ -103,10 +108,12 @@ export class AuthService {
|
||||
// We do nothing on failures. Reboot OpenSlides anyway.
|
||||
}
|
||||
this.router.navigate(['/']);
|
||||
await this.storageService.clear();
|
||||
await this.DS.clear();
|
||||
await this.operator.setWhoAmI(response);
|
||||
await this.OpenSlides.reboot();
|
||||
} else if (authType === 'saml') {
|
||||
await this.storageService.clear();
|
||||
await this.DS.clear();
|
||||
await this.operator.setWhoAmI(null);
|
||||
window.location.href = environment.urlPrefix + '/saml/?slo'; // Bye
|
||||
|
@ -3,9 +3,10 @@ import { Injectable } from '@angular/core';
|
||||
import { BaseModel } from '../../shared/models/base/base-model';
|
||||
import { CollectionStringMapperService } from './collection-string-mapper.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.
|
||||
*/
|
||||
@ -36,6 +37,19 @@ interface AutoupdateFormat {
|
||||
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}
|
||||
* Incoming objects, usually BaseModels, will be saved in the dataStore (`this.DS`)
|
||||
@ -45,6 +59,8 @@ interface AutoupdateFormat {
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AutoupdateService {
|
||||
private mutex = new Mutex();
|
||||
|
||||
/**
|
||||
* Constructor to create the AutoupdateService. Calls the constructor of the parent class.
|
||||
* @param websocketService
|
||||
@ -79,15 +95,17 @@ export class AutoupdateService {
|
||||
* Handles the change ids of all autoupdates.
|
||||
*/
|
||||
private async storeResponse(autoupdate: AutoupdateFormat): Promise<void> {
|
||||
const unlock = await this.mutex.lock();
|
||||
if (autoupdate.all_data) {
|
||||
await this.storeAllData(autoupdate);
|
||||
} else {
|
||||
await this.storePartialAutoupdate(autoupdate);
|
||||
}
|
||||
unlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores all data from the autoupdate. This means, that the DS is 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.
|
||||
* @param autoupdate The autoupdate
|
||||
*/
|
||||
@ -116,6 +134,22 @@ export class AutoupdateService {
|
||||
|
||||
// Normal autoupdate
|
||||
if (autoupdate.from_change_id <= maxChangeId + 1 && autoupdate.to_change_id > maxChangeId) {
|
||||
await this.injectAutupdateIntoDS(autoupdate, true);
|
||||
} else {
|
||||
// autoupdate fully in the future. we are missing something!
|
||||
console.log('Autoupdate in the future', maxChangeId, autoupdate.from_change_id, autoupdate.to_change_id);
|
||||
this.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
|
||||
@ -128,13 +162,11 @@ export class AutoupdateService {
|
||||
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);
|
||||
} else {
|
||||
// autoupdate fully in the future. we are missing something!
|
||||
this.requestChanges();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -160,9 +192,8 @@ export class AutoupdateService {
|
||||
* The server should return an autoupdate with all new data.
|
||||
*/
|
||||
public requestChanges(): void {
|
||||
const changeId = this.DS.maxChangeId === 0 ? 0 : this.DS.maxChangeId + 1;
|
||||
console.log(`requesting changed objects with DS max change id ${changeId}`);
|
||||
this.websocketService.send('getElements', { change_id: changeId });
|
||||
console.log(`requesting changed objects with DS max change id ${this.DS.maxChangeId}`);
|
||||
this.websocketService.send('getElements', { change_id: this.DS.maxChangeId });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -47,12 +47,11 @@ export class CollectionStringMapperService {
|
||||
* @param model
|
||||
*/
|
||||
public registerCollectionElement<V extends BaseViewModel<M>, M extends BaseModel>(
|
||||
collectionString: string,
|
||||
model: ModelConstructor<M>,
|
||||
viewModel: ViewModelConstructor<V>,
|
||||
repository: BaseRepository<V, M, TitleInformation>
|
||||
): 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({
|
||||
providedIn: 'root'
|
||||
@ -247,8 +248,17 @@ export class DataStoreUpdateManagerService {
|
||||
|
||||
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) {
|
||||
console.log('Concurrent update slots');
|
||||
const request = this.updateSlotRequests.pop();
|
||||
request.resolve();
|
||||
}
|
||||
@ -347,14 +357,21 @@ export class DataStoreService {
|
||||
|
||||
/**
|
||||
* Gets the DataStore from cache and instantiate all models out of the serialized version.
|
||||
* If something fails, the DS is cleared, so fresh data can be requrested from the server.
|
||||
*
|
||||
* @returns The max change id.
|
||||
*/
|
||||
public async initFromStorage(): Promise<number> {
|
||||
// This promise will be resolved with cached datastore.
|
||||
const store = await this.storageService.get<JsonStorage>(DataStoreService.cachePrefix + 'DS');
|
||||
if (store) {
|
||||
if (!store) {
|
||||
await this.clear();
|
||||
return this.maxChangeId;
|
||||
}
|
||||
|
||||
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this);
|
||||
|
||||
try {
|
||||
// There is a store. Deserialize it
|
||||
this.jsonStore = store;
|
||||
this.modelStore = this.deserializeJsonStore(this.jsonStore);
|
||||
@ -374,7 +391,8 @@ export class DataStoreService {
|
||||
});
|
||||
|
||||
this.DSUpdateManager.commit(updateSlot, maxChangeId, true);
|
||||
} else {
|
||||
} catch (e) {
|
||||
this.DSUpdateManager.dropUpdateSlot();
|
||||
await this.clear();
|
||||
}
|
||||
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 + '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 { OperatorService } from './operator.service';
|
||||
import { OperatorService, Permission } from './operator.service';
|
||||
|
||||
export interface AuthGuardFallbackEntry {
|
||||
route: string;
|
||||
weight: number;
|
||||
permission: string;
|
||||
permission: Permission;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -10,7 +10,7 @@ describe('HttpService', () => {
|
||||
});
|
||||
// TODO: Write a working Test
|
||||
// it('should be created', () => {
|
||||
// const service: HttpService = TestBed.get(HttpService);
|
||||
// const service: HttpService = TestBed.inject(HttpService);
|
||||
// expect(service).toBeTruthy();
|
||||
// });
|
||||
});
|
||||
|
@ -3,6 +3,7 @@ import { Injectable } from '@angular/core';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { AutoupdateFormat, AutoupdateService, isAutoupdateFormat } from './autoupdate.service';
|
||||
import { OpenSlidesStatusService } from './openslides-status.service';
|
||||
import { formatQueryParams, QueryParams } from '../definitions/query-params';
|
||||
|
||||
@ -17,12 +18,12 @@ export enum HTTPMethod {
|
||||
DELETE = 'delete'
|
||||
}
|
||||
|
||||
export interface DetailResponse {
|
||||
export interface ErrorDetailResponse {
|
||||
detail: string | string[];
|
||||
args?: string[];
|
||||
}
|
||||
|
||||
function isDetailResponse(obj: any): obj is DetailResponse {
|
||||
function isErrorDetailResponse(obj: any): obj is ErrorDetailResponse {
|
||||
return (
|
||||
obj &&
|
||||
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.
|
||||
*/
|
||||
@ -55,7 +65,8 @@ export class HttpService {
|
||||
public constructor(
|
||||
private http: HttpClient,
|
||||
private translate: TranslateService,
|
||||
private OSStatus: OpenSlidesStatusService
|
||||
private OSStatus: OpenSlidesStatusService,
|
||||
private autoupdateService: AutoupdateService
|
||||
) {
|
||||
this.defaultHeaders = new HttpHeaders().set('Content-Type', 'application/json');
|
||||
}
|
||||
@ -82,7 +93,7 @@ export class HttpService {
|
||||
): Promise<T> {
|
||||
// end early, if we are in history mode
|
||||
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.
|
||||
@ -108,9 +119,10 @@ export class HttpService {
|
||||
};
|
||||
|
||||
try {
|
||||
return await this.http.request<T>(method, url, options).toPromise();
|
||||
} catch (e) {
|
||||
throw this.handleError(e);
|
||||
const responseData: T = await this.http.request<T>(method, url, options).toPromise();
|
||||
return this.processResponse(responseData);
|
||||
} catch (error) {
|
||||
throw this.processError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,7 +132,7 @@ export class HttpService {
|
||||
* @param e The error thrown.
|
||||
* @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') + ': ';
|
||||
// If the error is a string already, return it.
|
||||
if (typeof e === 'string') {
|
||||
@ -142,15 +154,16 @@ export class HttpService {
|
||||
} else if (!e.error) {
|
||||
error += this.translate.instant("The server didn't respond.");
|
||||
} else if (typeof e.error === 'object') {
|
||||
if (isDetailResponse(e.error)) {
|
||||
error += this.processDetailResponse(e.error);
|
||||
if (isErrorDetailResponse(e.error)) {
|
||||
error += this.processErrorDetailResponse(e.error);
|
||||
} else {
|
||||
error = Object.keys(e.error)
|
||||
.map(key => {
|
||||
const errorList = Object.keys(e.error).map(key => {
|
||||
const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
|
||||
return this.translate.instant(capitalizedKey) + ': ' + this.processDetailResponse(e.error[key]);
|
||||
})
|
||||
.join(', ');
|
||||
return `${this.translate.instant(capitalizedKey)}: ${this.processErrorDetailResponse(
|
||||
e.error[key]
|
||||
)}`;
|
||||
});
|
||||
error = errorList.join(', ');
|
||||
}
|
||||
} else if (e.status === 500) {
|
||||
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.
|
||||
* @returns Error text(s) as single string
|
||||
*/
|
||||
private processDetailResponse(response: DetailResponse): string {
|
||||
private processErrorDetailResponse(response: ErrorDetailResponse): string {
|
||||
let message: string;
|
||||
if (response instanceof Array) {
|
||||
message = response.join(' ');
|
||||
} else if (response.detail instanceof Array) {
|
||||
if (response.detail instanceof Array) {
|
||||
message = response.detail.join(' ');
|
||||
} else {
|
||||
message = response.detail;
|
||||
@ -188,6 +199,14 @@ export class HttpService {
|
||||
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
|
||||
* @param path The path to send the request to.
|
||||
|
@ -2,6 +2,8 @@ import { Injectable } from '@angular/core';
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { Permission } from './operator.service';
|
||||
|
||||
/**
|
||||
* This represents one entry in the main menu
|
||||
*/
|
||||
@ -28,7 +30,7 @@ export interface MainMenuEntry {
|
||||
/**
|
||||
* The permission to see the entry.
|
||||
*/
|
||||
permission: string;
|
||||
permission: Permission;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,7 +1,11 @@
|
||||
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 { BannerDefinition, BannerService } from '../ui-services/banner.service';
|
||||
|
||||
/**
|
||||
* This service handles everything connected with being offline.
|
||||
*
|
||||
@ -16,6 +20,16 @@ export class OfflineService {
|
||||
* BehaviorSubject to receive further status values.
|
||||
*/
|
||||
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
|
||||
@ -33,7 +47,7 @@ export class OfflineService {
|
||||
if (!this.offline.getValue()) {
|
||||
console.log('offline because whoami failed.');
|
||||
}
|
||||
this.offline.next(true);
|
||||
this.goOffline();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,7 +57,15 @@ export class OfflineService {
|
||||
if (!this.offline.getValue()) {
|
||||
console.log('offline because connection lost.');
|
||||
}
|
||||
this.goOffline();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to set offline status
|
||||
*/
|
||||
private goOffline(): void {
|
||||
this.offline.next(true);
|
||||
this.banner.addBanner(this.bannerDefinition);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -51,5 +73,6 @@ export class OfflineService {
|
||||
*/
|
||||
public goOnline(): void {
|
||||
this.offline.next(false);
|
||||
this.banner.removeBanner(this.bannerDefinition);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
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
|
||||
@ -14,6 +15,9 @@ export class OpenSlidesStatusService {
|
||||
* in History mode, saves the history point.
|
||||
*/
|
||||
private history: History = null;
|
||||
private bannerDefinition: BannerDefinition = {
|
||||
type: 'history'
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns, if OpenSlides is in the history mode.
|
||||
@ -27,7 +31,7 @@ export class OpenSlidesStatusService {
|
||||
/**
|
||||
* Ctor, does nothing.
|
||||
*/
|
||||
public constructor() {}
|
||||
public constructor(private banner: BannerService) {}
|
||||
|
||||
/**
|
||||
* Calls the getLocaleString function of the history object, if present.
|
||||
@ -44,6 +48,7 @@ export class OpenSlidesStatusService {
|
||||
*/
|
||||
public enterHistoryMode(history: History): void {
|
||||
this.history = history;
|
||||
this.banner.addBanner(this.bannerDefinition);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -51,5 +56,6 @@ export class OpenSlidesStatusService {
|
||||
*/
|
||||
public leaveHistoryMode(): void {
|
||||
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.
|
||||
*/
|
||||
private async setupDataStoreAndWebSocket(): Promise<void> {
|
||||
let changeId = await this.DS.initFromStorage();
|
||||
if (changeId > 0) {
|
||||
changeId += 1;
|
||||
}
|
||||
const changeId = await this.DS.initFromStorage();
|
||||
// disconnect the WS connection, if there was one. This is needed
|
||||
// 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
|
||||
@ -141,7 +138,7 @@ export class OpenSlidesService {
|
||||
if (this.websocketService.isConnected) {
|
||||
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 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.
|
||||
@ -252,6 +285,10 @@ export class OperatorService implements OnAfterAppsLoaded {
|
||||
return response;
|
||||
}
|
||||
|
||||
public async clearWhoAmIFromStorage(): Promise<void> {
|
||||
await this.storageService.remove(WHOAMI_STORAGE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the operator user. Will be saved to storage
|
||||
* @param user The new operator.
|
||||
@ -390,12 +427,12 @@ export class OperatorService implements OnAfterAppsLoaded {
|
||||
} else {
|
||||
// Anonymous or users in the default group.
|
||||
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) {
|
||||
this.permissions = defaultGroup.permissions;
|
||||
}
|
||||
} else {
|
||||
const permissionSet = new Set<string>();
|
||||
const permissionSet = new Set<Permission>();
|
||||
this.DS.getMany(Group, this.user.groups_id).forEach(group => {
|
||||
group.permissions.forEach(permission => {
|
||||
permissionSet.add(permission);
|
||||
@ -416,6 +453,13 @@ export class OperatorService implements OnAfterAppsLoaded {
|
||||
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
|
||||
*/
|
||||
|
@ -40,7 +40,7 @@ export class PrioritizeService {
|
||||
if (this.openSlidesStatusService.isPrioritizedClient !== opPrioritized) {
|
||||
console.log('Alter prioritization:', 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 { ViewModelStoreService } from './view-model-store.service';
|
||||
|
||||
export interface ProjectorTitle {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This service cares about Projectables being projected and manage all projection-related
|
||||
* actions.
|
||||
@ -250,7 +255,7 @@ export class ProjectorService {
|
||||
projectorData.forEach(entry => {
|
||||
if (entry.data.error && entry.element.stable) {
|
||||
// Remove this element
|
||||
const idElementToRemove = this.slideManager.getIdentifialbeProjectorElement(entry.element);
|
||||
const idElementToRemove = this.slideManager.getIdentifiableProjectorElement(entry.element);
|
||||
elements = elements.filter(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)) {
|
||||
const idElement = this.slideManager.getIdentifialbeProjectorElement(element);
|
||||
const idElement = this.slideManager.getIdentifiableProjectorElement(element);
|
||||
const viewModel = this.getViewModelFromProjectorElement(idElement);
|
||||
if (viewModel) {
|
||||
return viewModel.getProjectorTitle();
|
||||
@ -338,7 +343,7 @@ export class ProjectorService {
|
||||
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,
|
||||
relation: RelationDefinition
|
||||
): 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;
|
||||
|
||||
const cacheProperty = '__' + property;
|
||||
@ -187,12 +198,24 @@ export class RelationManagerService {
|
||||
const _model: M = target.getModel();
|
||||
const relation = typeof property === 'string' ? relationsByKey[property] : null;
|
||||
|
||||
// try to find a getter for property
|
||||
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 getter was found in prototype chain, bind it with this proxy for right `this` access
|
||||
result = descriptor.get.bind(viewModel)();
|
||||
} else {
|
||||
result = target[property];
|
||||
// console.log(property, target);
|
||||
}
|
||||
} else if (property in _model) {
|
||||
result = _model[property];
|
||||
|
@ -13,7 +13,7 @@ describe('TimeTravelService', () => {
|
||||
);
|
||||
|
||||
it('should be created', () => {
|
||||
const service: TimeTravelService = TestBed.get(TimeTravelService);
|
||||
const service: TimeTravelService = TestBed.inject(TimeTravelService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
@ -55,14 +55,6 @@ export const WEBSOCKET_ERROR_CODES = {
|
||||
WRONG_FORMAT: 102
|
||||
};
|
||||
|
||||
/*
|
||||
* Options for (re-)connecting.
|
||||
*/
|
||||
interface ConnectOptions {
|
||||
changeId?: number;
|
||||
enableAutoupdates?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service that handles WebSocket connections. Other services can register themselfs
|
||||
* 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.
|
||||
*/
|
||||
public async connect(options: ConnectOptions = {}, retry: boolean = false): Promise<void> {
|
||||
const websocketId = Math.random()
|
||||
.toString(36)
|
||||
.substring(7);
|
||||
public async connect(changeId: number | null = null, retry: boolean = false): Promise<void> {
|
||||
const websocketId = Math.random().toString(36).substring(7);
|
||||
this.websocketId = websocketId;
|
||||
|
||||
if (this.websocket) {
|
||||
@ -222,17 +212,10 @@ export class WebsocketService {
|
||||
this.shouldBeClosed = false;
|
||||
}
|
||||
|
||||
// set defaults
|
||||
options = Object.assign(options, {
|
||||
enableAutoupdates: true
|
||||
});
|
||||
const queryParams: QueryParams = {};
|
||||
|
||||
const queryParams: QueryParams = {
|
||||
autoupdate: options.enableAutoupdates
|
||||
};
|
||||
|
||||
if (options.changeId !== undefined) {
|
||||
queryParams.change_id = options.changeId;
|
||||
if (changeId !== null) {
|
||||
queryParams.change_id = changeId;
|
||||
}
|
||||
|
||||
// Create the websocket
|
||||
@ -316,8 +299,9 @@ export class WebsocketService {
|
||||
const compressedSize = data.byteLength;
|
||||
const decompressedBuffer: Uint8Array = decompress(new Uint8Array(data));
|
||||
console.debug(
|
||||
`Recieved ${compressedSize / 1024} KB (${decompressedBuffer.byteLength /
|
||||
1024} KB uncompressed), ratio ${decompressedBuffer.byteLength / compressedSize}`
|
||||
`Recieved ${compressedSize / 1024} KB (${
|
||||
decompressedBuffer.byteLength / 1024
|
||||
} KB uncompressed), ratio ${decompressedBuffer.byteLength / compressedSize}`
|
||||
);
|
||||
data = this.arrayBufferToString(decompressedBuffer);
|
||||
}
|
||||
@ -399,7 +383,7 @@ export class WebsocketService {
|
||||
const timeout = Math.floor(Math.random() * 3000 + 2000);
|
||||
this.retryTimeout = setTimeout(() => {
|
||||
this.retryTimeout = null;
|
||||
this.connect({ enableAutoupdates: true }, true);
|
||||
this.connect(null, true);
|
||||
}, timeout);
|
||||
}
|
||||
}
|
||||
@ -439,9 +423,9 @@ export class WebsocketService {
|
||||
*
|
||||
* @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.connect(options);
|
||||
await this.connect(changeId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2,11 +2,8 @@ import { CommonModule } from '@angular/common';
|
||||
import { NgModule, Optional, SkipSelf, Type } from '@angular/core';
|
||||
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 { OperatorService } from './core-services/operator.service';
|
||||
import { PromptDialogComponent } from '../shared/components/prompt-dialog/prompt-dialog.component';
|
||||
|
||||
export const ServicesToLoadOnAppsLoaded: Type<OnAfterAppsLoaded>[] = [OperatorService];
|
||||
|
||||
@ -15,8 +12,7 @@ export const ServicesToLoadOnAppsLoaded: Type<OnAfterAppsLoaded>[] = [OperatorSe
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [CommonModule],
|
||||
providers: [Title],
|
||||
entryComponents: [PromptDialogComponent, ChoiceDialogComponent, ProjectionDialogComponent]
|
||||
providers: [Title]
|
||||
})
|
||||
export class CoreModule {
|
||||
/** 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';
|
||||
|
||||
interface BaseModelEntry {
|
||||
collectionString: string;
|
||||
repository: Type<BaseRepository<any, any, any>>;
|
||||
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 = {};
|
||||
if (styles && styles.length > 0) {
|
||||
for (const style of styles) {
|
||||
const styleDefinition = style
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(':');
|
||||
const styleDefinition = style.trim().toLowerCase().split(':');
|
||||
const key = styleDefinition[0];
|
||||
const value = styleDefinition[1];
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
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.
|
||||
* 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
|
||||
* open or download the pdf document
|
||||
* Use a local pdf service (i.e. MotionPdfService) to get the document definition for the content and
|
||||
* use this service to open or download the pdf document
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
@ -256,14 +256,11 @@ export class PdfDocumentService {
|
||||
if (logoHeaderLeftUrl && logoHeaderRightUrl) {
|
||||
text = '';
|
||||
} else {
|
||||
const general_event_name = this.configService.instant<string>('general_event_name');
|
||||
const general_event_description = this.configService.instant<string>('general_event_description');
|
||||
const line1 = [
|
||||
this.translate.instant(general_event_name),
|
||||
this.translate.instant(general_event_description)
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' – ');
|
||||
const general_event_name = this.translate.instant(this.configService.instant<string>('general_event_name'));
|
||||
const general_event_description = this.translate.instant(
|
||||
this.configService.instant<string>('general_event_description')
|
||||
);
|
||||
const line1 = [general_event_name, general_event_description].filter(Boolean).join(' - ');
|
||||
const line2 = [
|
||||
this.configService.instant('general_event_location'),
|
||||
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
|
||||
*
|
||||
|
@ -77,10 +77,14 @@ function addPageNumbers(data: any): void {
|
||||
|
||||
data.doc.footer = (currentPage, pageCount) => {
|
||||
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"
|
||||
if (footer.columns[0].stack[0] === '%PAGENR%' || countPageNumbers) {
|
||||
if (footer.columns[pageNumberColIndex]?.stack[0] === '%PAGENR%' || countPageNumbers) {
|
||||
countPageNumbers = true;
|
||||
footer.columns[0].stack[0] = `${currentPage} / ${pageCount}`;
|
||||
footer.columns[pageNumberColIndex].stack[0] = `${currentPage} / ${pageCount}`;
|
||||
}
|
||||
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 { ViewAssignment } from 'app/site/assignments/models/view-assignment';
|
||||
import {
|
||||
AgendaListTitle,
|
||||
BaseViewModelWithAgendaItem,
|
||||
isBaseViewModelWithAgendaItem
|
||||
} from 'app/site/base/base-view-model-with-agenda-item';
|
||||
import { ViewMotion } from 'app/site/motions/models/view-motion';
|
||||
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 { BaseHasContentObjectRepository } from '../base-has-content-object-repository';
|
||||
import { BaseIsAgendaItemContentObjectRepository } from '../base-is-agenda-item-content-object-repository';
|
||||
@ -33,6 +35,12 @@ const ItemRelations: RelationDefinition[] = [
|
||||
VForeignVerbose: 'BaseViewModelWithAgendaItem',
|
||||
ownContentObjectDataKey: 'contentObjectData',
|
||||
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');
|
||||
};
|
||||
|
||||
public getTitle = (titleInformation: ItemTitleInformation) => {
|
||||
private getAgendaTitle(titleInformation: ItemTitleInformation): AgendaListTitle {
|
||||
if (titleInformation.contentObject) {
|
||||
return titleInformation.contentObject.getAgendaListTitle();
|
||||
} else {
|
||||
@ -88,36 +96,14 @@ export class ItemRepositoryService extends BaseHasContentObjectRepository<
|
||||
) as BaseIsAgendaItemContentObjectRepository<any, any, any>;
|
||||
return repo.getAgendaListTitle(titleInformation.title_information);
|
||||
}
|
||||
}
|
||||
|
||||
public getTitle = (titleInformation: ItemTitleInformation) => {
|
||||
return this.getAgendaTitle(titleInformation).title;
|
||||
};
|
||||
|
||||
/**
|
||||
* Overrides the base function, if implemented.
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
public getSubtitle = (titleInformation: ItemTitleInformation) => {
|
||||
return this.getAgendaTitle(titleInformation).subtitle;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
*
|
||||
@ -169,8 +180,8 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit
|
||||
/**
|
||||
* 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 Item the target agenda item
|
||||
*/
|
||||
public async sortSpeakers(listOfSpeakers: ViewListOfSpeakers, speakerIds: number[]): Promise<void> {
|
||||
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 });
|
||||
}
|
||||
|
||||
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
|
||||
*
|
||||
|
@ -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] }));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: AssignmentRepositoryService = TestBed.get(AssignmentRepositoryService);
|
||||
const service: AssignmentRepositoryService = TestBed.inject(AssignmentRepositoryService);
|
||||
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 { RelationDefinition } from 'app/core/definitions/relations';
|
||||
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 { AssignmentTitleInformation, ViewAssignment } from 'app/site/assignments/models/view-assignment';
|
||||
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 { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
|
||||
import { ViewTag } from 'app/site/tags/models/view-tag';
|
||||
@ -35,6 +32,12 @@ const AssignmentRelations: RelationDefinition[] = [
|
||||
ownIdKey: 'attachments_id',
|
||||
ownKey: 'attachments',
|
||||
foreignViewModel: ViewMediafile
|
||||
},
|
||||
{
|
||||
type: 'O2M',
|
||||
ownKey: 'polls',
|
||||
foreignIdKey: 'assignment_id',
|
||||
foreignViewModel: ViewAssignmentPoll
|
||||
}
|
||||
];
|
||||
|
||||
@ -57,28 +60,6 @@ const AssignmentNestedModelDescriptors: NestedModelDescriptors = {
|
||||
getTitle: (viewAssignmentRelatedUser: ViewAssignmentRelatedUser) =>
|
||||
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
|
||||
> {
|
||||
private readonly restPath = '/rest/assignments/assignment/';
|
||||
private readonly restPollPath = '/rest/assignments/poll/';
|
||||
private readonly candidatureOtherPath = '/candidature_other/';
|
||||
private readonly candidatureSelfPath = '/candidature_self/';
|
||||
private readonly createPollPath = '/create_poll/';
|
||||
private readonly markElectedPath = '/mark_elected/';
|
||||
|
||||
/**
|
||||
* Constructor for the Assignment Repository.
|
||||
@ -179,87 +157,6 @@ export class AssignmentRepositoryService extends BaseIsAgendaItemAndListOfSpeake
|
||||
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
|
||||
*
|
||||
|
@ -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 { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
|
||||
import {
|
||||
AgendaListTitle,
|
||||
BaseViewModelWithAgendaItem,
|
||||
TitleInformationWithAgendaItem
|
||||
} 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
|
||||
const numberPrefix = titleInformation.agenda_item_number() ? `${titleInformation.agenda_item_number()} · ` : '';
|
||||
return numberPrefix + this.getTitle(titleInformation) + ' (' + this.getVerboseName() + ')';
|
||||
}
|
||||
|
||||
public getAgendaSubtitle(viewModel: V): string | null {
|
||||
return null;
|
||||
const title = numberPrefix + this.getTitle(titleInformation) + ' (' + this.getVerboseName() + ')';
|
||||
return { title };
|
||||
}
|
||||
|
||||
public getAgendaSlideTitle(titleInformation: T): string {
|
||||
@ -68,19 +66,8 @@ export abstract class BaseIsAgendaItemAndListOfSpeakersContentObjectRepository<
|
||||
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) => {
|
||||
return this.getAgendaListTitle(titleInformation);
|
||||
return this.getAgendaListTitle(titleInformation).title;
|
||||
};
|
||||
|
||||
public getListOfSpeakersSlideTitle = (titleInformation: T) => {
|
||||
@ -90,9 +77,7 @@ export abstract class BaseIsAgendaItemAndListOfSpeakersContentObjectRepository<
|
||||
protected createViewModelWithTitles(model: M): V {
|
||||
const viewModel = super.createViewModelWithTitles(model);
|
||||
viewModel.getAgendaListTitle = () => this.getAgendaListTitle(viewModel);
|
||||
viewModel.getAgendaListTitleWithoutItemNumber = () => this.getAgendaListTitleWithoutItemNumber(viewModel);
|
||||
viewModel.getAgendaSlideTitle = () => this.getAgendaSlideTitle(viewModel);
|
||||
viewModel.getAgendaSubtitle = () => this.getAgendaSubtitle(viewModel);
|
||||
viewModel.getListOfSpeakersTitle = () => this.getListOfSpeakersTitle(viewModel);
|
||||
viewModel.getListOfSpeakersSlideTitle = () => this.getListOfSpeakersSlideTitle(viewModel);
|
||||
return viewModel;
|
||||
|
@ -2,6 +2,7 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { ViewItem } from 'app/site/agenda/models/view-item';
|
||||
import {
|
||||
AgendaListTitle,
|
||||
BaseViewModelWithAgendaItem,
|
||||
TitleInformationWithAgendaItem
|
||||
} from 'app/site/base/base-view-model-with-agenda-item';
|
||||
@ -29,8 +30,7 @@ export interface IBaseIsAgendaItemContentObjectRepository<
|
||||
M extends BaseModel,
|
||||
T extends TitleInformationWithAgendaItem
|
||||
> extends BaseRepository<V, M, T> {
|
||||
getAgendaListTitle: (titleInformation: T) => string;
|
||||
getAgendaListTitleWithoutItemNumber: (titleInformation: T) => string;
|
||||
getAgendaListTitle: (titleInformation: T) => AgendaListTitle;
|
||||
getAgendaSlideTitle: (titleInformation: T) => string;
|
||||
}
|
||||
|
||||
@ -77,31 +77,11 @@ export abstract class BaseIsAgendaItemContentObjectRepository<
|
||||
* @returns the agenda title for the agenda item list. Should
|
||||
* 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
|
||||
const numberPrefix = titleInformation.agenda_item_number() ? `${titleInformation.agenda_item_number()} · ` : '';
|
||||
return numberPrefix + this.getTitle(titleInformation) + ' (' + this.getVerboseName() + ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() + ')';
|
||||
const title = numberPrefix + this.getTitle(titleInformation) + ' (' + this.getVerboseName() + ')';
|
||||
return { title };
|
||||
}
|
||||
|
||||
/**
|
||||
@ -117,9 +97,7 @@ export abstract class BaseIsAgendaItemContentObjectRepository<
|
||||
protected createViewModelWithTitles(model: M): V {
|
||||
const viewModel = super.createViewModelWithTitles(model);
|
||||
viewModel.getAgendaListTitle = () => this.getAgendaListTitle(viewModel);
|
||||
viewModel.getAgendaListTitleWithoutItemNumber = () => this.getAgendaListTitleWithoutItemNumber(viewModel);
|
||||
viewModel.getAgendaSlideTitle = () => this.getAgendaSlideTitle(viewModel);
|
||||
viewModel.getAgendaSubtitle = () => this.getAgendaSubtitle(viewModel);
|
||||
return viewModel;
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import { BaseViewModel, TitleInformation, ViewModelConstructor } from '../../sit
|
||||
import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service';
|
||||
import { DataSendService } from '../core-services/data-send.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 { OnAfterAppsLoaded } from '../definitions/on-after-apps-loaded';
|
||||
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>
|
||||
implements OnAfterAppsLoaded, Collection {
|
||||
implements OnAfterAppsLoaded, Collection, HasViewModelListObservable<V> {
|
||||
/**
|
||||
* 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> } = {};
|
||||
|
||||
/**
|
||||
* Observable subject for the whole list. These entries are unsorted an not piped through
|
||||
* autodTime. Just use this internally.
|
||||
* Observable subject for the whole list. These entries are unsorted and not piped through
|
||||
* auditTime. Just use this internally.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
protected createViewModelWithTitles(model: M): V {
|
||||
@ -269,7 +270,7 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
|
||||
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;
|
||||
|
||||
|
@ -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 mapperService Maps collection strings to classes
|
||||
|
@ -8,7 +8,7 @@ describe('FileRepositoryService', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({ imports: [E2EImportsModule] }));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: MediafileRepositoryService = TestBed.get(MediafileRepositoryService);
|
||||
const service: MediafileRepositoryService = TestBed.inject(MediafileRepositoryService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
import { ChangeRecoMode } from 'app/site/motions/motions.constants';
|
||||
import { BaseRepository } from '../base-repository';
|
||||
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 { 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 {ViewModelStoreService} viewModelStoreService
|
||||
* @param {TranslateService} translate
|
||||
* @param {RelationManagerService} relationManager
|
||||
* @param {DiffService} diffService
|
||||
* @param {LinenumberingService} lineNumbering Line numbering service
|
||||
*/
|
||||
public constructor(
|
||||
DS: DataStoreService,
|
||||
@ -59,7 +62,8 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
|
||||
viewModelStoreService: ViewModelStoreService,
|
||||
translate: TranslateService,
|
||||
relationManager: RelationManagerService,
|
||||
private diffService: DiffService
|
||||
private diffService: DiffService,
|
||||
private lineNumbering: LinenumberingService
|
||||
) {
|
||||
super(
|
||||
DS,
|
||||
@ -103,7 +107,7 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public getChangeRecoOfMotion(motion_id: number): ViewMotionChangeRecommendation[] {
|
||||
@ -171,22 +175,61 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
|
||||
* @param {LineRange} lineRange
|
||||
* @param {number} lineLength
|
||||
*/
|
||||
public createChangeRecommendationTemplate(
|
||||
public createMotionChangeRecommendationTemplate(
|
||||
motion: ViewMotion,
|
||||
lineRange: LineRange,
|
||||
lineLength: number
|
||||
): ViewMotionChangeRecommendation {
|
||||
const motionText = this.lineNumbering.insertLineNumbers(motion.text, lineLength);
|
||||
|
||||
const changeReco = new MotionChangeRecommendation();
|
||||
changeReco.line_from = lineRange.from;
|
||||
changeReco.line_to = lineRange.to;
|
||||
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.motion_id = motion.id;
|
||||
|
||||
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.
|
||||
* 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', () => {
|
||||
const service: MotionBlockRepositoryService = TestBed.get(MotionBlockRepositoryService);
|
||||
const service: MotionBlockRepositoryService = TestBed.inject(MotionBlockRepositoryService);
|
||||
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 { TreeIdNode } from 'app/core/ui-services/tree.service';
|
||||
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 { ViewUnifiedChange, ViewUnifiedChangeType } from 'app/shared/models/motions/view-unified-change';
|
||||
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 { ViewCategory } from 'app/site/motions/models/view-category';
|
||||
import { MotionTitleInformation, ViewMotion } from 'app/site/motions/models/view-motion';
|
||||
import { ViewMotionAmendedParagraph } from 'app/site/motions/models/view-motion-amended-paragraph';
|
||||
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
|
||||
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 { ViewStatuteParagraph } from 'app/site/motions/models/view-statute-paragraph';
|
||||
import { ViewSubmitter } from 'app/site/motions/models/view-submitter';
|
||||
@ -36,7 +37,7 @@ import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../bas
|
||||
import { NestedModelDescriptors } from '../base-repository';
|
||||
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.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';
|
||||
|
||||
@ -126,12 +127,17 @@ const MotionRelations: RelationDefinition[] = [
|
||||
ownKey: 'amendments',
|
||||
foreignViewModel: ViewMotion
|
||||
},
|
||||
// TMP:
|
||||
{
|
||||
type: 'M2O',
|
||||
ownIdKey: 'parent_id',
|
||||
ownKey: 'parent',
|
||||
foreignViewModel: ViewMotion
|
||||
},
|
||||
{
|
||||
type: 'O2M',
|
||||
foreignIdKey: 'motion_id',
|
||||
ownKey: 'polls',
|
||||
foreignViewModel: ViewMotionPoll
|
||||
}
|
||||
// Personal notes are dynamically added in the repo.
|
||||
];
|
||||
@ -195,11 +201,14 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
||||
* @param DS The DataStore
|
||||
* @param mapperService Maps collection strings to classes
|
||||
* @param dataSend sending changed objects
|
||||
* @param viewModelStoreService ViewModelStoreService
|
||||
* @param translate
|
||||
* @param relationManager
|
||||
* @param httpService OpenSlides own Http service
|
||||
* @param lineNumbering Line numbering for motion text
|
||||
* @param diff Display changes in motion text as diff.
|
||||
* @param personalNoteService service fo personal notes
|
||||
* @param config ConfigService (subscribe to sorting config)
|
||||
* @param operator
|
||||
*/
|
||||
public constructor(
|
||||
DS: DataStoreService,
|
||||
@ -264,46 +273,40 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
||||
public getAgendaListTitle = (titleInformation: MotionTitleInformation) => {
|
||||
const numberPrefix = titleInformation.agenda_item_number() ? `${titleInformation.agenda_item_number()} · ` : '';
|
||||
// Append the verbose name only, if not the special format 'Motion <identifier>' is used.
|
||||
let title;
|
||||
if (titleInformation.identifier) {
|
||||
return `${numberPrefix}${this.translate.instant('Motion')} ${titleInformation.identifier} · ${
|
||||
title = `${numberPrefix}${this.translate.instant('Motion')} ${titleInformation.identifier} · ${
|
||||
titleInformation.title
|
||||
}`;
|
||||
} else {
|
||||
return `${numberPrefix}${titleInformation.title} (${this.getVerboseName()})`;
|
||||
title = `${numberPrefix}${titleInformation.title} (${this.getVerboseName()})`;
|
||||
}
|
||||
};
|
||||
const agendaTitle: AgendaListTitle = { title };
|
||||
|
||||
/**
|
||||
* @override The base function and returns the submitters as optional subtitle.
|
||||
*/
|
||||
public getAgendaSubtitle = (motion: ViewMotion) => {
|
||||
if (motion.submittersAsUsers && motion.submittersAsUsers.length) {
|
||||
return `${this.translate.instant('by')} ${motion.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()})`;
|
||||
// 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
|
||||
const viewMotion: ViewMotion = titleInformation as ViewMotion;
|
||||
if (viewMotion.submittersAsUsers && viewMotion.submittersAsUsers.length) {
|
||||
agendaTitle.subtitle = `${this.translate.instant('by')} ${viewMotion.submittersAsUsers.join(', ')}`;
|
||||
}
|
||||
return agendaTitle;
|
||||
};
|
||||
|
||||
public getVerboseName = (plural: boolean = false) => {
|
||||
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 {
|
||||
const viewModel = super.createViewModelWithTitles(model);
|
||||
|
||||
viewModel.getIdentifierOrTitle = () => this.getIdentifierOrTitle(viewModel);
|
||||
viewModel.getProjectorTitle = () => this.getAgendaSlideTitle(viewModel);
|
||||
viewModel.getProjectorTitle = () => this.getProjectorTitle(viewModel);
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
@ -321,8 +324,19 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
||||
type: 'custom',
|
||||
ownKey: 'diffLines',
|
||||
get: (motion: Motion, viewMotion: ViewMotion) => {
|
||||
if (viewMotion.parent) {
|
||||
return this.getAmendmentParagraphs(viewMotion, this.motionLineLength, false);
|
||||
if (viewMotion.parent && viewMotion.isParagraphBasedAmendment()) {
|
||||
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
|
||||
@ -376,7 +390,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
||||
/**
|
||||
* Set the state of motions in bulk
|
||||
*
|
||||
* @param viewMotion target motion
|
||||
* @param viewMotions target motions
|
||||
* @param stateId the number that indicates the state
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @param viewMotion target motion
|
||||
* @param viewMotions target motions
|
||||
* @param motionblockId the number that indicates the motion block
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @param viewMotion target motion
|
||||
* @param viewMotions target motions
|
||||
* @param categoryId the number that indicates the category
|
||||
*/
|
||||
public async setMultiCategory(viewMotions: ViewMotion[], categoryId: number): Promise<void> {
|
||||
@ -609,11 +623,12 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
||||
case ChangeRecoMode.Diff:
|
||||
const text = [];
|
||||
const changesToShow = changes.filter(change => change.showInDiffView());
|
||||
const motionText = this.lineNumbering.insertLineNumbers(targetMotion.text, lineLength);
|
||||
|
||||
for (let i = 0; i < changesToShow.length; i++) {
|
||||
text.push(
|
||||
this.diff.extractMotionLineRange(
|
||||
targetMotion.text,
|
||||
motionText,
|
||||
{
|
||||
from: i === 0 ? 1 : changesToShow[i - 1].getLineTo(),
|
||||
to: changesToShow[i].getLineFrom()
|
||||
@ -624,18 +639,11 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
||||
)
|
||||
);
|
||||
|
||||
text.push(
|
||||
this.diff.getChangeDiff(targetMotion.text, changesToShow[i], lineLength, highlightLine)
|
||||
);
|
||||
text.push(this.diff.getChangeDiff(motionText, changesToShow[i], lineLength, highlightLine));
|
||||
}
|
||||
|
||||
text.push(
|
||||
this.diff.getTextRemainderAfterLastChange(
|
||||
targetMotion.text,
|
||||
changesToShow,
|
||||
lineLength,
|
||||
highlightLine
|
||||
)
|
||||
this.diff.getTextRemainderAfterLastChange(motionText, changesToShow, lineLength, highlightLine)
|
||||
);
|
||||
return text.join('');
|
||||
case ChangeRecoMode.Final:
|
||||
@ -715,46 +723,167 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
||||
* @param {number} lineLength
|
||||
*/
|
||||
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;
|
||||
return this.getTextParagraphs(parent, true, lineLength).map((paragraph: string, index: number) => {
|
||||
let localParagraph;
|
||||
if (motion.hasParent) {
|
||||
localParagraph = motion.amendment_paragraphs[index] ? motion.amendment_paragraphs[index] : paragraph;
|
||||
} else {
|
||||
localParagraph = paragraph;
|
||||
}
|
||||
return this.extractAffectedParagraphs(localParagraph, index);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {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 paragraphs that are affected by the given amendment in diff-format
|
||||
* 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
|
||||
* @returns {DiffLinesInParagraph}
|
||||
*/
|
||||
public getAmendmentParagraphs(
|
||||
public getAmendmentParagraphLines(
|
||||
amendment: ViewMotion,
|
||||
lineLength: number,
|
||||
crMode: ChangeRecoMode,
|
||||
changeRecommendations: ViewMotionChangeRecommendation[],
|
||||
includeUnchanged: boolean
|
||||
): DiffLinesInParagraph[] {
|
||||
const motion = amendment.parent;
|
||||
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
|
||||
|
||||
return (amendment.amendment_paragraphs || [])
|
||||
.map(
|
||||
let amendmentParagraphs;
|
||||
if (crMode === ChangeRecoMode.Changed) {
|
||||
amendmentParagraphs = this.applyChangesToAmendment(amendment, lineLength, changeRecommendations, true);
|
||||
} else {
|
||||
amendmentParagraphs = amendment.amendment_paragraphs || [];
|
||||
}
|
||||
|
||||
return amendmentParagraphs
|
||||
?.map(
|
||||
(newText: string, paraNo: number): DiffLinesInParagraph => {
|
||||
if (newText !== null) {
|
||||
return this.diff.getAmendmentParagraphsLinesByMode(
|
||||
return this.diff.getAmendmentParagraphsLines(
|
||||
paraNo,
|
||||
baseParagraphs[paraNo],
|
||||
newText,
|
||||
lineLength
|
||||
);
|
||||
} else {
|
||||
// Nothing has changed in this paragraph
|
||||
if (includeUnchanged) {
|
||||
return null; // Nothing has changed in this paragraph
|
||||
}
|
||||
}
|
||||
)
|
||||
.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,
|
||||
@ -767,27 +896,48 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
|
||||
textPost: ''
|
||||
} as DiffLinesInParagraph;
|
||||
} else {
|
||||
return null; // null will make this paragraph filtered out
|
||||
return diffLines;
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
.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.
|
||||
* 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 {number} lineLength
|
||||
* @param {ViewMotionChangeRecommendation[]} changeRecos
|
||||
* @returns {ViewMotionAmendedParagraph[]}
|
||||
*/
|
||||
public getAmendmentAmendedParagraphs(amendment: ViewMotion, lineLength: number): ViewMotionAmendedParagraph[] {
|
||||
public getAmendmentAmendedParagraphs(
|
||||
amendment: ViewMotion,
|
||||
lineLength: number,
|
||||
changeRecos: ViewMotionChangeRecommendation[]
|
||||
): ViewMotionAmendedParagraph[] {
|
||||
const motion = amendment.parent;
|
||||
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
|
||||
const changedAmendmentParagraphs = this.applyChangesToAmendment(amendment, lineLength, changeRecos, false);
|
||||
|
||||
return (amendment.amendment_paragraphs || [])
|
||||
.map(
|
||||
return changedAmendmentParagraphs
|
||||
?.map(
|
||||
(newText: string, paraNo: number): ViewMotionAmendedParagraph => {
|
||||
if (newText === 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,
|
||||
* but including line numbers relative to the original motion line numbers,
|
||||
* so they can be used for the amendment change recommendations
|
||||
*
|
||||
* @param {ViewMotion} amendment
|
||||
* @param {number} lineLength
|
||||
* @param {boolean} withDiff
|
||||
* @returns {LineNumberedString[]}
|
||||
*/
|
||||
public async createPoll(motion: ViewMotion): Promise<void> {
|
||||
const url = '/rest/motions/motion/' + motion.id + '/create_poll/';
|
||||
await this.httpService.post(url);
|
||||
public getAllAmendmentParagraphsWithOriginalLineNumbers(
|
||||
amendment: ViewMotion,
|
||||
lineLength: number,
|
||||
withDiff: boolean
|
||||
): LineNumberedString[] {
|
||||
const motion = amendment.parent;
|
||||
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
|
||||
|
||||
return (amendment.amendment_paragraphs || []).map((newText: string, paraNo: number): string => {
|
||||
const origText = baseParagraphs[paraNo];
|
||||
|
||||
if (newText === null) {
|
||||
return origText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an update request for a poll.
|
||||
*
|
||||
* @param poll
|
||||
*/
|
||||
public async updatePoll(poll: MotionPoll): Promise<void> {
|
||||
const url = '/rest/motions/motion-poll/' + poll.id + '/';
|
||||
const data = {
|
||||
motion_id: poll.motion_id,
|
||||
id: poll.id,
|
||||
votescast: poll.votescast,
|
||||
votesvalid: poll.votesvalid,
|
||||
votesinvalid: poll.votesinvalid,
|
||||
votes: {
|
||||
Yes: poll.yes,
|
||||
No: poll.no,
|
||||
Abstain: poll.abstain
|
||||
}
|
||||
};
|
||||
await this.httpService.put(url, data);
|
||||
}
|
||||
const diff = this.diff.diff(origText, newText);
|
||||
|
||||
/**
|
||||
* Sends a http request to delete the given poll
|
||||
*
|
||||
* @param poll
|
||||
*/
|
||||
public async deletePoll(poll: MotionPoll): Promise<void> {
|
||||
const url = '/rest/motions/motion-poll/' + poll.id + '/';
|
||||
await this.httpService.delete(url);
|
||||
if (withDiff) {
|
||||
return diff;
|
||||
} else {
|
||||
return this.diff.diffHtmlToFinalText(diff);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { HttpService } from 'app/core/core-services/http.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> {
|
||||
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', () => {
|
||||
const service = TestBed.get(TagRepositoryService);
|
||||
const service = TestBed.inject(TagRepositoryService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
@ -11,7 +11,7 @@ describe('TopicRepositoryService', () => {
|
||||
);
|
||||
|
||||
it('should be created', () => {
|
||||
const service: TopicRepositoryService = TestBed.get(TopicRepositoryService);
|
||||
const service: TopicRepositoryService = TestBed.inject(TopicRepositoryService);
|
||||
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 { Topic } from 'app/shared/models/topics/topic';
|
||||
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 { 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) => {
|
||||
return titleInformation.title;
|
||||
};
|
||||
|
||||
public getListTitle = (titleInformation: TopicTitleInformation) => {
|
||||
if (titleInformation.agenda_item_number && titleInformation.agenda_item_number()) {
|
||||
return `${titleInformation.agenda_item_number()} · ${titleInformation.title}`;
|
||||
} else {
|
||||
return titleInformation.title;
|
||||
return this.getTitle(titleInformation);
|
||||
}
|
||||
};
|
||||
|
||||
public getAgendaListTitle = (titleInformation: TopicTitleInformation) => {
|
||||
// Do not append ' (Topic)' to the title.
|
||||
return this.getTitle(titleInformation);
|
||||
return { title: this.getListTitle(titleInformation) };
|
||||
};
|
||||
|
||||
public getAgendaSlideTitle = (titleInformation: TopicTitleInformation) => {
|
||||
// Do not append ' (Topic)' to the title.
|
||||
return this.getTitle(titleInformation);
|
||||
};
|
||||
|
||||
/**
|
||||
* @override The base function.
|
||||
*
|
||||
* @returns The plain title.
|
||||
*/
|
||||
public getAgendaListTitleWithoutItemNumber = (titleInformation: TopicTitleInformation) => {
|
||||
return titleInformation.title;
|
||||
return this.getAgendaListTitle(titleInformation).title;
|
||||
};
|
||||
|
||||
public getVerboseName = (plural: boolean = false) => {
|
||||
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 { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
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 { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||
import { Group } from 'app/shared/models/users/group';
|
||||
@ -16,9 +19,9 @@ import { DataStoreService } from '../../core-services/data-store.service';
|
||||
/**
|
||||
* Shape of a permission
|
||||
*/
|
||||
interface Permission {
|
||||
interface PermDefinition {
|
||||
display_name: string;
|
||||
value: string;
|
||||
value: Permission;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -26,7 +29,7 @@ interface Permission {
|
||||
*/
|
||||
export interface AppPermissions {
|
||||
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');
|
||||
};
|
||||
|
||||
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.
|
||||
*
|
||||
* @param group The group
|
||||
* @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);
|
||||
return await this.http.post(`/rest/${group.collectionString}/${group.id}/set_permission/`, {
|
||||
perm: perm,
|
||||
@ -93,7 +103,7 @@ export class GroupRepositoryService extends BaseRepository<ViewGroup, Group, Gro
|
||||
* @param perm certain permission as string
|
||||
* @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]) {
|
||||
this.appPermissions[appId] = {
|
||||
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 { environment } from '../../../../environments/environment';
|
||||
|
||||
export interface MassImportResult {
|
||||
importedTrackIds: number[];
|
||||
errors: { [id: number]: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* type for determining the user name from a string during import.
|
||||
* See {@link parseUserString} for implementations
|
||||
@ -125,6 +130,18 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
||||
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) => {
|
||||
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 {
|
||||
const viewModel = super.createViewModelWithTitles(model);
|
||||
viewModel.getFullName = () => this.getFullName(viewModel);
|
||||
viewModel.getShortName = () => this.getShortName(viewModel);
|
||||
viewModel.getLevelAndNumber = () => this.getLevelAndNumber(viewModel);
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
@ -209,15 +227,11 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
||||
*
|
||||
* @param newEntries
|
||||
*/
|
||||
public async bulkCreate(newEntries: NewEntry<User>[]): Promise<number[]> {
|
||||
public async bulkCreate(newEntries: NewEntry<User>[]): Promise<MassImportResult> {
|
||||
const data = newEntries.map(entry => {
|
||||
return { ...entry.newEntry, importTrackId: entry.importTrackId };
|
||||
});
|
||||
const response = (await this.httpService.post(`/rest/users/user/mass_import/`, { users: data })) as {
|
||||
detail: string;
|
||||
importedTrackIds: number[];
|
||||
};
|
||||
return response.importedTrackIds;
|
||||
return await this.httpService.post<MassImportResult>(`/rest/users/user/mass_import/`, { users: data });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -293,7 +307,8 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
||||
} else if (numEmails === 1) {
|
||||
msg = this.translate.instant('One email was send sucessfully.');
|
||||
} 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) {
|
||||
@ -375,7 +390,7 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
||||
* @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)
|
||||
*/
|
||||
public parseUserString(inputUser: string, schema?: StringNamingSchema): User {
|
||||
public parseUserString(inputUser: string, schema: StringNamingSchema = 'firstSpaceLast'): User {
|
||||
const newUser: Partial<User> = {};
|
||||
if (schema === 'lastCommaFirst') {
|
||||
const commaSeparated = inputUser.split(',');
|
||||
@ -390,7 +405,7 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
|
||||
default:
|
||||
newUser.first_name = inputUser;
|
||||
}
|
||||
} else if (!schema || schema === 'firstSpaceLast') {
|
||||
} else if (schema === 'firstSpaceLast') {
|
||||
const splitUser = inputUser.split(' ');
|
||||
switch (splitUser.length) {
|
||||
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
|
||||
@ -30,6 +30,12 @@ _('Front page text');
|
||||
_('[Space for your welcome text.]');
|
||||
_('System');
|
||||
_('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');
|
||||
_('OpenSlides Theme');
|
||||
_('Export');
|
||||
@ -58,6 +64,13 @@ _('PDF footer logo (left)');
|
||||
_('PDF footer logo (right)');
|
||||
_('Web interface header 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
|
||||
_('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');
|
||||
_('Couple countdown with the list of speakers');
|
||||
_('[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)');
|
||||
_('public');
|
||||
_('internal');
|
||||
@ -99,7 +113,8 @@ _('Only main agenda items');
|
||||
_('Topics');
|
||||
_('Open requests to speak');
|
||||
|
||||
// Motions config strings
|
||||
// ** Motions **
|
||||
// config strings
|
||||
// subgroup general
|
||||
_('General');
|
||||
_('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');
|
||||
// subgroup 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');
|
||||
_('All valid ballots');
|
||||
@ -176,12 +192,8 @@ _('Custom number of ballot papers');
|
||||
_('PDF export');
|
||||
_('Title 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');
|
||||
// misc motion strings
|
||||
_('Amendment');
|
||||
_('Statute amendment for');
|
||||
_('Statute paragraphs');
|
||||
|
||||
// motion workflow 1
|
||||
_('Simple Workflow');
|
||||
@ -224,46 +236,7 @@ _('Needs review');
|
||||
_('rejected (not authorized)');
|
||||
_('Reject (not authorized)');
|
||||
_('Rejection (not authorized)');
|
||||
// misc for motions
|
||||
_('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
|
||||
// motion workflow manager
|
||||
_('Recommendation label');
|
||||
_('Allow support');
|
||||
_('Allow create poll');
|
||||
@ -275,11 +248,67 @@ _('Show amendment in parent motion');
|
||||
_('Restrictions');
|
||||
_('Label color');
|
||||
_('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');
|
||||
_('Voting');
|
||||
_('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 **
|
||||
// permission strings (see models.py of each Django app)
|
||||
@ -303,6 +332,7 @@ _('Can manage tags');
|
||||
_('Can manage configuration');
|
||||
_('Can manage logos and fonts');
|
||||
_('Can see history');
|
||||
_('Can see the live stream');
|
||||
// mediafiles
|
||||
_('Can see the list of files');
|
||||
_('Can upload files');
|
||||
@ -318,9 +348,11 @@ _('Can see comments');
|
||||
_('Can manage comments');
|
||||
_('Can manage motion metadata');
|
||||
_('Can create amendments');
|
||||
_('Can manage motion polls');
|
||||
|
||||
// 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 change its own password');
|
||||
|
||||
@ -328,6 +360,9 @@ _('Can change its own password');
|
||||
_('General');
|
||||
_('Sort name of participants by');
|
||||
_('Enable participant presence view');
|
||||
_('Activate vote weight');
|
||||
_('Allow users to set themselves as present');
|
||||
_('e.g. for online meetings');
|
||||
_('Participants');
|
||||
_('Given name');
|
||||
_('Surname');
|
||||
@ -356,7 +391,7 @@ _('OpenSlides access data');
|
||||
_('You can use {event_name} and {username} as placeholder.');
|
||||
_('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.');
|
||||
_(
|
||||
@ -392,7 +427,27 @@ _('OpenSlides is temporarily reset to following timestamp');
|
||||
_('Motion change recommendation created');
|
||||
_('Motion change recommendation updated');
|
||||
_('Motion change recommendation deleted');
|
||||
_('Motion block set to');
|
||||
_('Poll created');
|
||||
_('Poll updated');
|
||||
_('Poll deleted');
|
||||
_('Comment {arg1} updated');
|
||||
|
||||
// core misc strings
|
||||
_('items per page');
|
||||
_('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]
|
||||
})
|
||||
export class OpenSlidesTranslateModule {
|
||||
public static forRoot(): ModuleWithProviders {
|
||||
public static forRoot(): ModuleWithProviders<TranslateModule> {
|
||||
return {
|
||||
ngModule: TranslateModule,
|
||||
providers: [
|
||||
@ -46,7 +46,7 @@ export class OpenSlidesTranslateModule {
|
||||
}
|
||||
|
||||
// no config store for child.
|
||||
public static forChild(): ModuleWithProviders {
|
||||
public static forChild(): ModuleWithProviders<TranslateModule> {
|
||||
return {
|
||||
ngModule: TranslateModule,
|
||||
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_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 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) {
|
||||
return true;
|
||||
}
|
||||
} else if (typeof item[filter.property] === 'function') {
|
||||
return item[filter.property]() === option.condition;
|
||||
} else if (item[filter.property] === option.condition) {
|
||||
return true;
|
||||
} else if (item[filter.property].toString() === option.condition) {
|
||||
|
@ -26,7 +26,7 @@ export interface NewEntry<V> {
|
||||
newEntry: V;
|
||||
status: CsvImportStatus;
|
||||
errors: string[];
|
||||
hasDuplicates: boolean;
|
||||
hasDuplicates?: boolean;
|
||||
importTrackId?: number;
|
||||
}
|
||||
|
||||
@ -179,7 +179,6 @@ export abstract class BaseImportService<M extends BaseModel> {
|
||||
|
||||
/**
|
||||
* Clears all stored secondary data
|
||||
* TODO: Merge with clearPreview()
|
||||
*/
|
||||
public abstract clearData(): void;
|
||||
|
||||
@ -190,7 +189,6 @@ export abstract class BaseImportService<M extends BaseModel> {
|
||||
* @param file
|
||||
*/
|
||||
public parseInput(file: string): void {
|
||||
this.clearData();
|
||||
this.clearPreview();
|
||||
const papaConfig: ParseConfig = {
|
||||
header: false,
|
||||
@ -205,28 +203,7 @@ export abstract class BaseImportService<M extends BaseModel> {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
entryLines.forEach(line => {
|
||||
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._entries = entryLines.map(x => this.mapData(x)).filter(x => !!x);
|
||||
this.newEntries.next(this._entries);
|
||||
this.updatePreview();
|
||||
}
|
||||
@ -238,6 +215,21 @@ export abstract class BaseImportService<M extends BaseModel> {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@ -293,7 +285,7 @@ export abstract class BaseImportService<M extends BaseModel> {
|
||||
// TODO: error message for wrong file type (test Firefox on Windows!)
|
||||
if (event.target.files && event.target.files.length === 1) {
|
||||
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 {
|
||||
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 {
|
||||
this.reader.readAsText(file, this.encoding);
|
||||
private readFile(): void {
|
||||
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)
|
||||
*/
|
||||
public clearPreview(): void {
|
||||
this.clearData();
|
||||
this._entries = [];
|
||||
this.newEntries.next([]);
|
||||
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
|
||||
*/
|
||||
public setError(entry: NewEntry<M>, error: string): void {
|
||||
if (this.errorList.hasOwnProperty(error)) {
|
||||
if (this.errorList[error]) {
|
||||
if (!entry.errors) {
|
||||
entry.errors = [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)
|
||||
// it('should be created', () => {
|
||||
// const service: BaseSortService = TestBed.get(BaseSortService);
|
||||
// const service: BaseSortService = TestBed.inject(BaseSortService);
|
||||
// expect(service).toBeTruthy();
|
||||
// });
|
||||
});
|
||||
|
@ -13,7 +13,7 @@ describe('ChoiceService', () => {
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
const service: ChoiceService = TestBed.get(ChoiceService);
|
||||
const service: ChoiceService = TestBed.inject(ChoiceService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
@ -11,7 +11,6 @@ interface CountUserRequest {
|
||||
|
||||
export interface CountUserData {
|
||||
userId: number;
|
||||
usesIndexedDB: boolean;
|
||||
}
|
||||
|
||||
interface CountUserResponse extends CountUserRequest {
|
||||
@ -49,8 +48,7 @@ export class CountUsersService {
|
||||
{
|
||||
token: request.content.token,
|
||||
data: {
|
||||
userId: this.currentUserId,
|
||||
usesIndexedDB: true
|
||||
userId: this.currentUserId
|
||||
}
|
||||
},
|
||||
request.senderChannelName
|
||||
|
@ -754,7 +754,10 @@ describe('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 =
|
||||
"<P>rief sie alle sieben herbei und sprach 'liebe Kinder, ich will hinaus in den Wald, seid </P>",
|
||||
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', () => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
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';
|
||||
|
||||
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.
|
||||
*/
|
||||
interface CommonAncestorData {
|
||||
@ -34,11 +35,13 @@ interface CommonAncestorData {
|
||||
*/
|
||||
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[];
|
||||
/**
|
||||
* 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[];
|
||||
/**
|
||||
@ -109,7 +112,8 @@ export interface LineRange {
|
||||
/**
|
||||
* The end line number.
|
||||
* 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;
|
||||
}
|
||||
@ -167,7 +171,9 @@ export interface DiffLinesInParagraph {
|
||||
*
|
||||
* ```ts
|
||||
* 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 extractUntil = 3;
|
||||
* 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:
|
||||
*
|
||||
* ```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);
|
||||
* ```
|
||||
*
|
||||
@ -205,7 +212,11 @@ export interface DiffLinesInParagraph {
|
||||
*
|
||||
* ```ts
|
||||
* 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);
|
||||
* ```
|
||||
*/
|
||||
@ -885,10 +896,7 @@ export class DiffService {
|
||||
}
|
||||
}
|
||||
|
||||
return str
|
||||
.replace(/^\s+/g, '')
|
||||
.replace(/\s+$/g, '')
|
||||
.replace(/ {2,}/g, ' ');
|
||||
return str.replace(/^\s+/g, '').replace(/\s+$/g, '').replace(/ {2,}/g, ' ');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1000,14 +1008,7 @@ export class DiffService {
|
||||
classes = childElement.getAttribute('class').split(' ');
|
||||
}
|
||||
classes.push(className);
|
||||
childElement.setAttribute(
|
||||
'class',
|
||||
classes
|
||||
.sort()
|
||||
.join(' ')
|
||||
.replace(/^\s+/, '')
|
||||
.replace(/\s+$/, '')
|
||||
);
|
||||
childElement.setAttribute('class', classes.sort().join(' ').replace(/^\s+/, '').replace(/\s+$/, ''));
|
||||
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
|
||||
* @return {string}
|
||||
@ -1150,10 +1152,7 @@ export class DiffService {
|
||||
let html = this.serializeTag(node);
|
||||
for (let i = 0; i < node.childNodes.length; i++) {
|
||||
if (node.childNodes[i].nodeType === TEXT_NODE) {
|
||||
html += node.childNodes[i].nodeValue
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
html += node.childNodes[i].nodeValue.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
} else if (
|
||||
!stripLineNumbers ||
|
||||
(!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
|
||||
* 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"
|
||||
* @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
|
||||
* 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"
|
||||
* @param {Node} node
|
||||
@ -1296,7 +1297,8 @@ export class DiffService {
|
||||
* Returns the HTML snippet between two given line numbers.
|
||||
* extractRangeByLineNumbers
|
||||
* 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
|
||||
*
|
||||
* 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.
|
||||
* - 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.
|
||||
* 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>:
|
||||
* - 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 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} toLine
|
||||
* @returns {ExtractedContent}
|
||||
*/
|
||||
public extractRangeByLineNumbers(htmlIn: string, fromLine: number, toLine: number): ExtractedContent {
|
||||
public extractRangeByLineNumbers(htmlIn: LineNumberedString, fromLine: number, toLine: number): ExtractedContent {
|
||||
if (typeof htmlIn !== 'string') {
|
||||
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.
|
||||
* 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
|
||||
* @returns {LineRange}
|
||||
@ -1867,25 +1871,35 @@ export class DiffService {
|
||||
|
||||
// Performing the actual diff
|
||||
const str = this.diffString(workaroundPrepend + htmlOld, workaroundPrepend + htmlNew);
|
||||
let diffUnnormalized = str
|
||||
.replace(/^\s+/g, '')
|
||||
.replace(/\s+$/g, '')
|
||||
.replace(/ {2,}/g, ' ');
|
||||
let diffUnnormalized = str.replace(/^\s+/g, '').replace(/\s+$/g, '').replace(/ {2,}/g, ' ');
|
||||
|
||||
diffUnnormalized = this.fixWrongChangeDetection(diffUnnormalized);
|
||||
|
||||
// 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
|
||||
diffUnnormalized = diffUnnormalized.replace(
|
||||
/<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 => {
|
||||
return (br !== undefined ? br : '') + span + ' </span>';
|
||||
/<del>(((<BR CLASS="os-line-break">)<\/del><del>)?(<span[^>]+os-line-number[^>]+?>)(\s|<\/?del>)*<\/span>)<\/del>/gi,
|
||||
(found: string, tag: string, brWithDel: string, plainBr: string, span: string): string => {
|
||||
return (plainBr !== undefined ? plainBr : '') + span + ' </span>';
|
||||
}
|
||||
);
|
||||
|
||||
// Merging individual insert/delete statements into bigger blocks
|
||||
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,
|
||||
// but only of these specific characters
|
||||
diffUnnormalized = diffUnnormalized.replace(
|
||||
@ -2116,8 +2130,10 @@ export class DiffService {
|
||||
});
|
||||
|
||||
changes.forEach((change: ViewUnifiedChange) => {
|
||||
if (!change.isTitleChange()) {
|
||||
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);
|
||||
@ -2135,7 +2151,7 @@ export class DiffService {
|
||||
* @param {number} lineLength the line length
|
||||
* @return {DiffLinesInParagraph|null}
|
||||
*/
|
||||
public getAmendmentParagraphsLinesByMode(
|
||||
public getAmendmentParagraphsLines(
|
||||
paragraphNo: number,
|
||||
origText: string,
|
||||
newText: string,
|
||||
@ -2187,20 +2203,18 @@ export class DiffService {
|
||||
* Returns the HTML with the changes, optionally with a highlighted line.
|
||||
* The original motion needs to be provided.
|
||||
*
|
||||
* @param {string} motionHtml
|
||||
* @param {LineNumberedString} html
|
||||
* @param {ViewUnifiedChange} change
|
||||
* @param {number} lineLength
|
||||
* @param {number} highlight
|
||||
* @returns {string}
|
||||
*/
|
||||
public getChangeDiff(
|
||||
motionHtml: string,
|
||||
html: LineNumberedString,
|
||||
change: ViewUnifiedChange,
|
||||
lineLength: number,
|
||||
highlight?: number
|
||||
): string {
|
||||
const html = this.lineNumberingService.insertLineNumbers(motionHtml, lineLength);
|
||||
|
||||
let data, oldText;
|
||||
|
||||
try {
|
||||
@ -2245,14 +2259,14 @@ export class DiffService {
|
||||
/**
|
||||
* Returns the remainder text of the motion after the last change
|
||||
*
|
||||
* @param {string} motionHtml
|
||||
* @param {LineNumberedString} motionHtml
|
||||
* @param {ViewUnifiedChange[]} changes
|
||||
* @param {number} lineLength
|
||||
* @param {number} highlight
|
||||
* @returns {string}
|
||||
*/
|
||||
public getTextRemainderAfterLastChange(
|
||||
motionHtml: string,
|
||||
motionHtml: LineNumberedString,
|
||||
changes: ViewUnifiedChange[],
|
||||
lineLength: number,
|
||||
highlight?: number
|
||||
@ -2264,15 +2278,14 @@ export class DiffService {
|
||||
}
|
||||
}, 0);
|
||||
|
||||
const numberedHtml = this.lineNumberingService.insertLineNumbers(motionHtml, lineLength, highlight);
|
||||
if (changes.length === 0) {
|
||||
return numberedHtml;
|
||||
return motionHtml;
|
||||
}
|
||||
|
||||
let data;
|
||||
|
||||
try {
|
||||
data = this.extractRangeByLineNumbers(numberedHtml, maxLine, null);
|
||||
data = this.extractRangeByLineNumbers(motionHtml, maxLine, null);
|
||||
} catch (e) {
|
||||
// This only happens (as far as we know) when the motion text has been altered (shortened)
|
||||
// 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
|
||||
*
|
||||
* @param {string} motionText
|
||||
* @param {LineNumberedString} motionText
|
||||
* @param {LineRange} lineRange
|
||||
* @param {boolean} lineNumbers - weather to add line numbers to the returned HTML string
|
||||
* @param {number} lineLength
|
||||
* @param {number|null} highlightedLine
|
||||
*/
|
||||
public extractMotionLineRange(
|
||||
motionText: string,
|
||||
motionText: LineNumberedString,
|
||||
lineRange: LineRange,
|
||||
lineNumbers: boolean,
|
||||
lineLength: number,
|
||||
highlightedLine: number
|
||||
): string {
|
||||
const origHtml = this.lineNumberingService.insertLineNumbers(motionText, lineLength, highlightedLine);
|
||||
const extracted = this.extractRangeByLineNumbers(origHtml, lineRange.from, lineRange.to);
|
||||
const extracted = this.extractRangeByLineNumbers(motionText, lineRange.from, lineRange.to);
|
||||
let html =
|
||||
extracted.outerContextStart +
|
||||
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 { DurationService } from './duration.service';
|
||||
|
||||
describe('DurationService', () => {
|
||||
beforeEach(() =>
|
||||
let service: DurationService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [DurationService]
|
||||
})
|
||||
);
|
||||
|
||||
it('should be created', inject([DurationService], (service: DurationService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
}),
|
||||
(service = TestBed.inject(DurationService));
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*
|
||||
@ -70,10 +88,12 @@ export class DurationService {
|
||||
* @returns a more human readable time representation
|
||||
*/
|
||||
public durationToString(duration: number, suffix: 'h' | 'm'): string {
|
||||
const major = Math.floor(duration / 60);
|
||||
const minor = `0${duration % 60}`.slice(-2);
|
||||
if (!isNaN(+major) && !isNaN(+minor)) {
|
||||
return `${major}:${minor} ${suffix}`;
|
||||
const negative = duration < 0;
|
||||
const major = negative ? Math.ceil(duration / 60) : Math.floor(duration / 60);
|
||||
const minor = `0${Math.abs(duration) % 60}`.slice(-2);
|
||||
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 {
|
||||
return '';
|
||||
}
|
||||
|
@ -3,6 +3,11 @@ import { Injectable } from '@angular/core';
|
||||
const ELEMENT_NODE = 1;
|
||||
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
|
||||
* exceeds the maximum line length.
|
||||
@ -29,7 +34,8 @@ export interface LineNumberRange {
|
||||
/**
|
||||
* The end line number.
|
||||
* 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;
|
||||
}
|
||||
@ -67,7 +73,9 @@ interface SectionHeading {
|
||||
*
|
||||
* Removing line numbers from a line-numbered string:
|
||||
* ```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);
|
||||
* ```
|
||||
*
|
||||
@ -118,7 +126,8 @@ export class LinenumberingService {
|
||||
// The line number counter
|
||||
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;
|
||||
|
||||
// A workaround to prevent double line numbers
|
||||
@ -368,8 +377,11 @@ export class LinenumberingService {
|
||||
* @returns {LineNumberRange}
|
||||
*/
|
||||
public getLineNumberRange(html: string): LineNumberRange {
|
||||
const cacheKey = this.djb2hash(html);
|
||||
let range = this.lineNumberCache.get(cacheKey);
|
||||
if (!range) {
|
||||
const fragment = this.htmlToFragment(html);
|
||||
const range = {
|
||||
range = {
|
||||
from: null,
|
||||
to: null
|
||||
};
|
||||
@ -384,6 +396,8 @@ export class LinenumberingService {
|
||||
range.to = number + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.lineNumberCache.put(cacheKey, range);
|
||||
return range;
|
||||
}
|
||||
|
||||
@ -473,10 +487,17 @@ export class LinenumberingService {
|
||||
* @return {string[]}
|
||||
*/
|
||||
public splitToParagraphs(html: string): string[] {
|
||||
const cacheKey = this.djb2hash(html);
|
||||
let cachedParagraphs = this.lineNumberCache.get(cacheKey);
|
||||
if (!cachedParagraphs) {
|
||||
const fragment = this.htmlToFragment(html);
|
||||
return this.splitNodeToParagraphs(fragment).map((node: Element): string => {
|
||||
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,
|
||||
callback?: () => void,
|
||||
firstLine?: number
|
||||
): string {
|
||||
): LineNumberedString {
|
||||
let newHtml, newRoot;
|
||||
|
||||
if (highlight > 0) {
|
||||
|
@ -18,7 +18,7 @@ interface ImageConfigObject {
|
||||
/**
|
||||
* The structure of a font config
|
||||
*/
|
||||
interface FontConfigObject {
|
||||
export interface FontConfigObject {
|
||||
display_name: string;
|
||||
default: string;
|
||||
path: string;
|
||||
|
@ -12,7 +12,7 @@ describe('OverlayService', () => {
|
||||
);
|
||||
|
||||
it('should be created', () => {
|
||||
const service: OverlayService = TestBed.get(OverlayService);
|
||||
const service: OverlayService = TestBed.inject(OverlayService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatDialog, MatDialogRef } from '@angular/material';
|
||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
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({}));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: ProgressService = TestBed.get(ProgressService);
|
||||
const service: ProgressService = TestBed.inject(ProgressService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
@ -12,7 +12,7 @@ export class ThemeService {
|
||||
/**
|
||||
* 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.
|
||||
@ -54,7 +54,7 @@ export class ThemeService {
|
||||
this.currentTheme = theme;
|
||||
|
||||
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) {
|
||||
classList.remove(...toRemove); // Remove all old themes.
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ describe('TreeSortService', () => {
|
||||
|
||||
// TODO testing (does not work without injecting a BaseViewComponent)
|
||||
// it('should be created', () => {
|
||||
// const service: TreeSortService = TestBed.get(TreeSortService);
|
||||
// const service: TreeSortService = TestBed.inject(TreeSortService);
|
||||
// expect(service).toBeTruthy();
|
||||
// });
|
||||
});
|
||||
|
@ -354,7 +354,8 @@ export class TreeService {
|
||||
*
|
||||
* @param item The current item from which the flat node will be created.
|
||||
* @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.
|
||||
*/
|
||||
|
@ -3,8 +3,6 @@ import { SwUpdate, UpdateAvailableEvent } from '@angular/service-worker';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { NotifyService } from '../core-services/notify.service';
|
||||
|
||||
/**
|
||||
* Handle Service Worker updates using the SwUpdate service form angular.
|
||||
*/
|
||||
@ -12,8 +10,6 @@ import { NotifyService } from '../core-services/notify.service';
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UpdateService {
|
||||
private static NOTIFY_NAME = 'swCheckForUpdate';
|
||||
|
||||
/**
|
||||
* @returns the updateSubscription
|
||||
*/
|
||||
@ -28,12 +24,7 @@ export class UpdateService {
|
||||
* @param swUpdate Service Worker update service
|
||||
* @param matSnackBar Currently to show that an update is available
|
||||
*/
|
||||
public constructor(private swUpdate: SwUpdate, private notify: NotifyService) {
|
||||
// Listen on requests from other users to check for updates.
|
||||
this.notify.getMessageObservable(UpdateService.NOTIFY_NAME).subscribe(() => {
|
||||
this.checkForUpdate();
|
||||
});
|
||||
}
|
||||
public constructor(private swUpdate: SwUpdate) {}
|
||||
|
||||
/**
|
||||
* Manually applies the update if one was found
|
||||
@ -52,13 +43,4 @@ export class UpdateService {
|
||||
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