Compare commits

...

412 Commits
main ... 3.2

Author SHA1 Message Date
Emanuel Schütze 9aefb122e6 Release 3.2 2020-07-15 16:16:00 +02:00
Emanuel Schütze 5618c04416 Use twisted[tls] in production.txt to install required packages. 2020-07-15 16:15:39 +02:00
Emanuel Schütze ee344032b7
Merge pull request #5472 from emanuelschuetze/changelog
Updated Changelog for 3.2
2020-07-15 14:15:52 +02:00
Emanuel Schütze 6e80ff5f00 Updated Changelog for 3.2 2020-07-15 12:45:16 +02:00
Emanuel Schütze 47113f14fc
Merge pull request #5470 from tsiegleauq/cannot-click-bg
Fix clicking behind conference bar
2020-07-15 12:43:10 +02:00
Sean 7d912d82de Fix clicking behind conference bar
Fixes an issue with the live stream.
Clicks in the background were falsely cought.
2020-07-14 15:10:01 +02:00
Emanuel Schütze ebf8325ded
Merge pull request #5468 from tsiegleauq/safari-list-views
Fix list views for safari
2020-07-14 14:28:08 +02:00
Emanuel Schütze 03acae26ff
Merge pull request #5465 from tsiegleauq/better-jitsi-icons
Add link to CLOS to meeting room indicator
2020-07-14 14:24:54 +02:00
Emanuel Schütze 271ccdd46a
Merge pull request #5467 from tsiegleauq/pdf-error-free-text-amendment
Fix PDF generation for motion with free amendments
2020-07-14 14:15:47 +02:00
Sean 109fea791d Fix list views for safari
Usually useless hight information fixes rendering issues on safari
browsers
2020-07-14 13:13:49 +02:00
Sean c2bd7c16a9 Fix PDF generation for motion with free amendments
Fixes an issue where motions which had free form amendments were not be
able to render as pdf
2020-07-14 12:34:05 +02:00
Sean 7ded2cd8a1 Add link to CLOS to meeting room indicator
Links the "cannot enter meeting room"-indicator to the CLOS.
Changes the minimize icon
2020-07-14 10:53:02 +02:00
Emanuel Schütze 85a22ed99c
Merge pull request #5329 from tsiegleauq/changelog-3.2
Changelog for 3.2
2020-07-13 14:59:57 +02:00
Sean e2597002e2 Changelog for 3.2 2020-07-12 22:13:20 +02:00
Emanuel Schütze 01ce1409d3
Merge pull request #5463 from emanuelschuetze/translations-20200712
Updated translations (DE, CZ, RU)
2020-07-12 18:04:17 +02:00
Emanuel Schütze 74e3ea119e Updated translations (DE, CZ, RU) 2020-07-12 17:41:06 +02:00
Emanuel Schütze 9e55cb1480
Merge pull request #5462 from tsiegleauq/more-jitsi-improvements
Improve Jitsi UI
2020-07-12 17:22:32 +02:00
Emanuel Schütze 20175a1a6b
Merge pull request #5461 from tsiegleauq/list-of-speaker-slide-fix
Add prefix to topic list of speaker slide
2020-07-12 17:16:53 +02:00
Sean 719d1d1cf1 Improve Jitsi UI
Some slight jitsi/stream look and feel improvements

The "closed door" now has a tooltip "add yourseld to los to enter
conference"
Use "keyboard_hide" to indicate minimize/hide
Hide the potentially obsolete quick controls when the full jitsi dialog
is open
Use the "voice_chat" icon to indicate showing the full jitsi dialog
Remove "hangup" "raise hand" and "help" from the jitsi iframe
Add hangup and leave to the jitsi dialog in replacement of hangup in the
iFrame
The "enter live conference" icon is now highlighted by a fade animation
2020-07-10 16:46:29 +02:00
Sean 2835e746e8 Add prefix to topic list of speaker slide
The topics prefix (number) was missing in the list of speakers slide
2020-07-09 12:56:58 +02:00
Emanuel Schütze a7703a5557
Merge pull request #5454 from tsiegleauq/whole-agenda-slide-misses-top
Add top to agenda projection
2020-07-09 09:25:36 +02:00
Emanuel Schütze da4092768e
Merge pull request #5453 from tsiegleauq/use-a-tag-for-external-jitsi
Use a-tag for external link
2020-07-09 08:31:06 +02:00
Sean 32775b0a2a Add top to agenda projection
Show the agenda title prefix in the whole agenda slide projection
2020-07-09 08:16:57 +02:00
Sean 011c23093f Use a-tag for external link
Allows to left-click, right-click and middle-click the "open external"
button. Left clicking will disconnect from the current jitsi connection
in OpenSlides. Useful for tests and power using, such as multiple jitsi
connections or easier copying the link.
2020-07-09 08:11:39 +02:00
Emanuel Schütze 3063a9e9fc
Merge pull request #5452 from tsiegleauq/tiny-jitsi
Improve Jitsi on mobile
2020-07-09 08:10:23 +02:00
Sean 656fcccee1 Improve Jitsi on mobile
Improves Jitsi and various other stuff for small to smallest devices.
The mobile main action button is a little higher and aligns with the
jitsi bar.
Some mat-cards have extra space to be more jitsi-bar-friendly.
Hides the list filter option on phones
2020-07-08 13:43:54 +02:00
Emanuel Schütze e35b658731
Merge pull request #5456 from emanuelschuetze/fixTypo
Remove debug output
2020-07-08 11:58:46 +02:00
Emanuel Schütze d76d74e225 Remove debug output 2020-07-08 11:43:39 +02:00
Finn Stutzenstein 9eeb287425
Merge pull request #5455 from tsiegleauq/another-travis-fix
Pin isort, repair tests
2020-07-08 07:16:27 +02:00
Sean 5666749e62 Pin isort, repair tests
Pins isort for testing and execute all tests
2020-07-07 16:14:08 +02:00
Finn Stutzenstein eeb97c44fd
Merge pull request #5450 from ApolloLV/patch-1
Update nodeJS to 12.x in Dockerfile
2020-07-03 13:57:55 +02:00
Emanuel Schütze fa1347f611
Merge pull request #5451 from emanuelschuetze/default-workflow
Fixed config value for default workflow
2020-07-03 09:14:14 +02:00
Emanuel Schütze 278b33c2d7 Fixed config value for default workflow 2020-07-02 23:47:26 +02:00
ApolloLV 1cb8ef2d14
Update nodeJS to 12.x in Dockerfile
The Angular CLI requires a minimum Node.js version of either v10.13 or v12.0
2020-07-02 22:25:49 +02:00
Emanuel Schütze ba3c5e07f7
Merge pull request #5449 from emanuelschuetze/translations-20200702
Updated translations
2020-07-02 22:05:25 +02:00
Emanuel Schütze 55f1d02fcc
Merge pull request #5429 from tsiegleauq/main-page-cleanup
Cleanup main routing page and ngrid lists
2020-07-02 22:04:53 +02:00
Emanuel Schütze 378d091dbd
Merge pull request #5448 from tsiegleauq/fix-duration-for-ff-headless
Fix duration service for non chrome browsers
2020-07-02 21:46:38 +02:00
Emanuel Schütze cb8f219163 Updated translations 2020-07-02 21:44:22 +02:00
Emanuel Schütze 66757b04ae
Merge pull request #5446 from tsiegleauq/login-ignore-warning
Add "login anyway" button
2020-07-02 21:38:35 +02:00
Sean 346413fbb0 Fix duration service for non chrome browsers
-0, 0 and other uses of negative values in the duration service hat a
change of producing undesired results.
Added tests and fixed the function
2020-07-02 15:52:31 +02:00
Finn Stutzenstein cb190331f3
Merge pull request #5447 from tsiegleauq/allow-negative-duration
Allow negative duration
2020-07-02 11:43:25 +02:00
Sean 23ee6a2951 Allow negative duration
Allow negative values for durationToString.
The use case is fairly constructed, but now logically correct
2020-07-01 17:44:33 +02:00
Sean f59ce9ef3b
Merge pull request #5444 from FinnStutzenstein/fixDependencyFromCoreToSite
Fixed import of the clos into the operator
2020-07-01 13:26:26 +02:00
Sean f5654f3a8c Add "login anyway" button
Skip browser warning easier.
2020-07-01 12:22:37 +02:00
FinnStutzenstein 4a96aa31c1
Fixed import of the clos into the operator 2020-06-29 09:25:50 +02:00
Emanuel Schütze fab51091b1
Merge pull request #5443 from tsiegleauq/pdf-left-footer
Render left pdf footer image and page number
2020-06-27 12:59:55 +02:00
Emanuel Schütze c1d63b320d
Merge pull request #5442 from tsiegleauq/jitsi-iframe-control-adjustment
Always show the toolbar in jitsi iframe
2020-06-27 12:55:37 +02:00
Sean 988ee0fe93 Render left pdf footer image and page number
Using the web worker, the PDF page number in combination with a left
footer image could not be rendered together.
2020-06-26 14:31:22 +02:00
Sean 3d252060c9 Always show the toolbar in jitsi iframe
Try to prevent that the toolbar hides itself
2020-06-26 10:35:26 +02:00
Emanuel Schütze 6898458695
Merge pull request #5437 from tsiegleauq/another-fake-dialog
Replace jitsi mat dialog
2020-06-25 17:50:39 +02:00
Sean c2a1b62c8b Replace jitsi mat dialog
Replace Jitsis Mat Dialog with an div container
Fixes an issue where observables in jitsi would register multiple times
Clear the IndexedDB on logout
2020-06-25 16:34:36 +02:00
Sean bb10c25974
Merge pull request #5438 from tsiegleauq/clean-deps
Updates some npm deps, fixes travis
2020-06-23 12:28:27 +02:00
Sean fde745530e Updates some npm deps, fixes travis
Another thing for travis
2020-06-23 12:12:01 +02:00
Emanuel Schütze 9a47cff7fa
Merge pull request #5435 from emanuelschuetze/translations20200619
Updated translations
2020-06-19 17:29:04 +02:00
Sean 22a374a150 Cleanup main routing page and ngrid lists
Sets the main container to absolute,
makes it easier to detect the pages height
ngrid lists can dynamically detect the page height using flexbox,
remove external class injection from list-views,
add "spacer-bottom-60" class for the jitsi-container,
add it everywhere where the jitsi container could hide the content.
2020-06-19 16:47:40 +02:00
Emanuel Schütze f70953f454
Merge pull request #5434 from tsiegleauq/fix-poll-progress
Fix poll progress
2020-06-19 16:37:56 +02:00
Emanuel Schütze 435f555559 Updated translations 2020-06-19 16:29:52 +02:00
Sean 9cf602f0c1 Fix poll progress
Fixes a regression in the poll progress.
Poll progress war no longer working for assignments
2020-06-19 13:26:23 +02:00
Emanuel Schütze 2fd4e70b0c
Merge pull request #5433 from tsiegleauq/import-doubled-topics
Allow to import the same topic multiple times
2020-06-18 23:05:03 +02:00
Emanuel Schütze 81b021ab47
Merge pull request #5432 from FinnStutzenstein/fixSaml
Fix SAML default group ids setting
2020-06-18 17:56:50 +02:00
FinnStutzenstein fd371b87e4
Fix SAML default group ids setting 2020-06-18 15:58:19 +02:00
Emanuel Schütze e20c93d445
Merge pull request #5430 from tsiegleauq/stream-jitsi-perms
separate stream permission from jitsi
2020-06-18 15:57:15 +02:00
Sean 55f65576f0 Allow to import the same topic multiple times
Remove checking for duplicated topic names.
Allows to import topics more than once, usefull if you have to import an
agenda over and over again.
2020-06-18 15:51:51 +02:00
Emanuel Schütze d558c293b2
Merge pull request #5424 from jsangmeister/delegate-view-fixes
Fix some delegate view bugs
2020-06-18 15:51:08 +02:00
Joshua Sangmeister 44f1d1e819 Fix some delegate view bugs 2020-06-18 15:26:50 +02:00
Sean 677595fe5b
Merge pull request #5431 from tsiegleauq/dark-theme-improvements
Improve dark themes
2020-06-18 13:35:17 +02:00
Sean 912a528f8a Improve dark themes
Add better contrast to dark themes on various instances
Add yellow as accent color for red themes
2020-06-18 13:11:45 +02:00
Sean 9feaa59ebb Seperate stream permission from jitsi
Allows jitsi conferences without stream permission,
allowing detailed speration from users who can jitsi streams and see
stream
2020-06-18 12:06:59 +02:00
Emanuel Schütze b712af2d6d
Merge pull request #5427 from tsiegleauq/amendments-without-id
Use title for amendments if an id does not exist
2020-06-17 15:56:37 +02:00
Emanuel Schütze 81c2df3458
Merge pull request #5428 from FinnStutzenstein/fixAsgirefVersion
Use newer asgiref release than the broken 3.2.8 one
2020-06-17 15:51:02 +02:00
FinnStutzenstein 6a59e678a9
Use newer asgiref release than the broken 3.2.8 one 2020-06-17 15:38:07 +02:00
Sean 00e644292d Use title for amendments if an id does not exist
Also some slight head-bar cleanups
2020-06-17 11:08:22 +02:00
Emanuel Schütze b43151fd59
Merge pull request #5425 from tsiegleauq/motion-csv-impexp
Export motion id latest
2020-06-16 22:17:04 +02:00
Sean fbbc4389fb Export motion id latest
Put the motion id to the end of all export used.
Solves the issue that users cannot re-import CSVs if they export them
with sequential numbers
2020-06-16 21:35:24 +02:00
Emanuel Schütze d53e85b853
Merge pull request #5423 from emanuelschuetze/rename-permission
Rename and check "can see extra data" permission.
2020-06-16 21:15:56 +02:00
Emanuel Schütze 68c77fe52c Rename and check "can see extra data" permission. 2020-06-16 21:00:37 +02:00
Sean bc1373b696
Merge pull request #5416 from tsiegleauq/solarized-dark-theme
Enhance dark theme
2020-06-16 13:25:43 +02:00
Sean b9fbf4209b Cleanup theming related code
Enhance color usage, add more default themes, add solarized theme
2020-06-16 12:56:08 +02:00
Sean ec2ec08333
Merge pull request #5422 from tsiegleauq/some-python-updates
Fix python 3.6 and 3.7 tests
2020-06-16 12:50:27 +02:00
Sean 958f0fb786 Fix python 3.6 and 3.7 tests
Hotfix for python tests
2020-06-16 12:36:25 +02:00
Emanuel Schütze ac4cb39105
Merge pull request #5419 from tsiegleauq/agenda-slide-distance
Add more distance for topics in agenda slide
2020-06-15 15:12:52 +02:00
Emanuel Schütze b5bc855dfe
Merge pull request #5417 from tsiegleauq/agenda-numbered-topics-pdf
Prevent double "TOP" in agenda PDF
2020-06-15 15:10:22 +02:00
Sean 1f876ec6dd Prevent double "TOP" in agenda pdf
Also: changes the definition of "getTitle" and "getAgendaTitle" to be more
consistent and easier to guess. getTitle will simply return the title, while getAgendaTitle
returns the title with the agenda-prefix
2020-06-12 16:18:57 +02:00
Emanuel Schütze c1605929e9
Merge pull request #5410 from tsiegleauq/better-login-page
Add better login page
2020-06-12 15:13:41 +02:00
Emanuel Schütze 2ea95937d7
Merge pull request #5420 from tsiegleauq/user-set-present-link-dup
Remove presence link in user multi select mode
2020-06-12 15:09:50 +02:00
Emanuel Schütze a80915397d
Merge pull request #5418 from FinnStutzenstein/fixSamlUserCreation
Fix Saml II and saml default groups
2020-06-12 14:55:51 +02:00
Sean f06f2dee9f Remove presence link in user multi select mode
Removes the link to "users/presence" while in the user lists multi
selection view. The link was duplicated to be present in both multi
select and single selct view
2020-06-12 14:51:05 +02:00
Sean 33ba8c4628 Add more distance for topics in agenda slide
Makes the projection of the agenda looks cleaner and more relaxed
2020-06-12 14:43:40 +02:00
FinnStutzenstein dc7dfc1936
Fix Saml II and saml default groups 2020-06-12 14:37:43 +02:00
Sean 7d3280707d
Merge pull request #5414 from tsiegleauq/support-chrome-80
Lower chrome/chromium support version to version
2020-06-12 11:08:26 +02:00
Sean 13cbece9d9 Lower chrome/chromium support version to version
Lowers the supported version for chrome and chromium based browsers to
80. Keep in mind that you can always skip the checks!
2020-06-12 10:50:36 +02:00
Sean 5ed9c88ae4 Add better login page
Themes now cover the login page
more responsive on both large, small and smalest screens
the footer behaves like a footer
2020-06-12 10:23:10 +02:00
Finn Stutzenstein 5239e40858
Merge pull request #5412 from FinnStutzenstein/fixMigrationPath
Fix migration path for non-existing fonts
2020-06-12 08:13:11 +02:00
FinnStutzenstein 081f13e2ff
Fix migration path for non-existing fonts 2020-06-12 07:26:29 +02:00
Emanuel Schütze 438b3558bf
Merge pull request #5411 from emanuelschuetze/translations-20200511
Updated translations.
2020-06-11 17:46:13 +02:00
Emanuel Schütze ff4324117e Updated translations. 2020-06-11 17:23:42 +02:00
Emanuel Schütze f590994875
Merge pull request #5403 from tsiegleauq/blacklist-browsers
Catch unsupported browsers
2020-06-11 15:42:45 +02:00
Emanuel Schütze 2cdb3f4ef3
Merge pull request #5407 from tsiegleauq/assignment-pdf-reduce
Fix assignment pdf results
2020-06-11 15:10:36 +02:00
Emanuel Schütze e3c1d5432b
Merge pull request #5408 from tsiegleauq/monospace-font-fix
Add roboto-condensed-bold as default
2020-06-11 15:04:28 +02:00
Sean 9387a3f394 Catch unsupported browsers
unspoorted browsers trying to access the login mask will be forwarded to
an info page.
The info page shows that the browser is not suppoted and hints the smallest
supported version of their current browser.
As it works best and might prevent some support calls, I added an hint
for chrome as the favored browser by OpenSlides (debateable)

To update/downgrade the supported versions, simply edit the enum in the
service.

If we cannot detect the browser, we assume it was supported.
2020-06-11 14:39:14 +02:00
Sean 1853028cf0 Add roboto-condensed-bold as default
Fixes a severe regression. New databases could not generate any PDF or use
the countdown.
2020-06-11 14:30:01 +02:00
Sean 56b47214bc Fix assignment pdf results
Filters out results unfitting to the current election method.
2020-06-11 14:12:10 +02:00
Emanuel Schütze 43b13e314e
Merge pull request #5376 from tsiegleauq/integrate-streams
Integrate streams
2020-06-11 13:53:04 +02:00
Sean 0d9738b72d Integrate streams
Integrate live streaming inside the jitsi/rtc components.
Live streaming works without jitsi, but is using the same components for
a fluid integration.
A streaming URL can be set in the settings page.
Users EITHER consume the live stream OR are presend in a jitsi live
conference.

To consume both the live stream and the jitsi conference, users may use
a dedicated jitsi tab in their session.

The jitsi users can be restricted to only allow thouse with the right
the manage speakers or being present on the "current list of speakers",
automatically simulating a virtual plenum
2020-06-11 11:20:00 +02:00
Emanuel Schütze 47795b57d1
Merge pull request #5405 from FinnStutzenstein/fixSamlUserCreation
Fix the creation of saml users
2020-06-10 15:32:03 +02:00
FinnStutzenstein 7d455b34f5
Fix the creation of saml users
When created, they are put into the cache.
Also allows bulk-delete for saml users.
2020-06-10 15:06:13 +02:00
Emanuel Schütze fbb0be6fb4
Merge pull request #5404 from emanuelschuetze/whitelist
Updated server whitelist of allowed styles.
2020-06-09 23:15:17 +02:00
Emanuel Schütze acf499f6e1
Merge pull request #5378 from jsangmeister/monospace-countdown
Replaces the countdown font with a monospaced one
2020-06-09 23:15:06 +02:00
Emanuel Schütze 79e3780a26 Updated server whitelist of allowed styles.
added 'vertical-align'
2020-06-09 22:59:41 +02:00
Joshua Sangmeister e653021eff Add a configurable monospace font for the countdown
use Roboto Condensed bold by default
2020-06-09 22:54:31 +02:00
Emanuel Schütze aeb893a8d9
Merge pull request #5399 from tsiegleauq/normal-amendments-in-list
Fix error for freely editable amendments
2020-06-09 21:48:13 +02:00
Emanuel Schütze 82efbe76bd
Merge pull request #5356 from flowluap/patch-2
added proxy pass to nginx configuration file
2020-06-09 21:22:08 +02:00
Emanuel Schütze ff9125fb9f
Merge pull request #5401 from tsiegleauq/assignment-poll-chart-abstain
Hide abstain bar in assignment polls chart
2020-06-09 21:20:54 +02:00
Sean d4f211e344 Fix error for freely editable amendments
Recent changes to amendment had various bugs.
This one regarded the amendment list, where amendments without amendmend
paragraphs returned several errors.
2020-06-09 21:16:04 +02:00
Emanuel Schütze 4673c741e9
Merge pull request #5402 from jsangmeister/fix-tiny-mce
Fix change detection for Tiny MCE
2020-06-09 20:56:29 +02:00
Emanuel Schütze e1345cb808
Merge pull request #5398 from CatoTH/bugfix-non-paragraph-based-amendments
Bugfix non paragraph based amendments
2020-06-09 20:53:03 +02:00
Joshua Sangmeister bf35c55956 Fix change detection for Tiny MCE 2020-06-09 13:14:05 +02:00
Sean 6efdc9a3dd Hide abstain bar in assignment polls chart
If the percent base does not include "abstain", the grey abstain bar
will not be shown in the assignment poll chart
2020-06-09 12:47:38 +02:00
Tobias Hößl cadef6d42e
Don't show amendment CR-diff-view if no CRs exist 2020-06-07 15:02:35 +02:00
Tobias Hößl bc3b8be78d
Increase recalculation performance by additional caching 2020-06-07 11:44:07 +02:00
Tobias Hößl 18bc495bd8
Bugfix: errors with non-paragraph-based amendments 2020-06-07 10:54:57 +02:00
Emanuel Schütze 8451cd2d88
Merge pull request #5395 from emanuelschuetze/translation-20200604
Updated translations
2020-06-04 17:06:55 +02:00
Emanuel Schütze 5072e66a7e Updated translations 2020-06-04 17:06:08 +02:00
Emanuel Schütze 3109337004
Merge pull request #5394 from tsiegleauq/jitsi-iframe-dialog
Show Jitsi iFrame in Dialog
2020-06-04 16:37:01 +02:00
Sean 3ca4714812 Show Jitsi iFrame in Dialog
Replace the external link button with a (real) Dialog containing the
jitsi iFrame.
The dialog can be hidden but not entirly closed.
Replace all dialogService.closeAll() functions by closing the specific
dialog rather than all of them.
2020-06-04 16:29:35 +02:00
FinnStutzenstein 429473dcf9
Merge branch 'pollprogressCd' 2020-06-04 15:16:20 +02:00
FinnStutzenstein c186a575f6
Fixed incomplete autoupdates
A conceptional issue in `get_data_since` leads to incomplete
autoupdates. The behaviour was long time in the code, but only with a
lot of autoupdates (high concurrency) and the autoupdate delay I noticed
the bug during testing. I'm sure, that this issue might have caused
incomplete autoupdates (which the user may experience as "lost
autoupdates") in previous productive instances. Instead of quering a
range (from_change_id to to_change_id) one now can only get data from a
change id up to the max change id in the element cache. The max change
id gets now returned by `get_data_since`.

I also added a get_all_data with the capability of returning the
max_change_id at this point of time.

As a usability-"fix" (more like a fix the result of a bug, not the bug
itself) a refresh button for a poll was added, that issues an autoupdate
for the poll and all options.
2020-06-04 15:16:05 +02:00
Emanuel Schütze c4f482b70c
Merge pull request #5391 from jsangmeister/update-poll-slide
Fixes motion poll slide icon size & rounding
2020-06-03 19:16:29 +02:00
Emanuel Schütze 0275df6ab2
Merge pull request #5392 from tsiegleauq/vote-await-server-answer
Wait for server while voting
2020-06-03 17:35:19 +02:00
Sean dced8fbcc7 Wait for server while voting
Blocks voting state changes and prevents the user from sending multiple
vote values while the server is not responding during voting
2020-06-03 14:53:31 +02:00
Joshua Sangmeister f4907e6604 Fixes motion poll slide icon size & rounding 2020-06-02 22:41:23 +02:00
Emanuel Schütze d7408b40f9
Merge pull request #5390 from jsangmeister/hide-amendments-when-deactivated
Hides amendments entry in menu if deactivated
2020-06-02 22:26:14 +02:00
Finn Stutzenstein e215a23b80
Merge pull request #5389 from jsangmeister/reverse-motion-relations
Adds reverse relations for motions and blocks
2020-06-02 16:26:07 +02:00
Joshua Sangmeister a31fa7dda6 Adds reverse relations for motions and blocks 2020-06-02 14:44:56 +02:00
Emanuel Schütze 7665634d42
Merge pull request #5375 from FinnStutzenstein/autoupdatePerformance
Autoupdate performance
2020-05-29 17:31:32 +02:00
Finn Stutzenstein 9c7b9b0920
Merge pull request #5387 from FinnStutzenstein/hugeautoupdatesInRedis
Inserting changed and deleted elements into redis in batches (fixes #5386)
2020-05-29 15:48:21 +02:00
FinnStutzenstein 0eee839736
Small improvements and first attempt to make to poll progress responsive
to massive autoupdates. The "optimization" didn't help, so this has to
be continued in another PR.
2020-05-29 15:46:19 +02:00
Joshua Sangmeister a84bfccd07 Hides amendments entry in menu if deactivated 2020-05-28 15:26:56 +02:00
FinnStutzenstein 600b9c148b
Inserting changed and deleted elements into redis in batches (fixes #5386) 2020-05-28 14:00:57 +02:00
FinnStutzenstein d8b21c5fb5
(WIP) Ordered and delayed autoupdates:
- Extracted autoupdate code from consumers
- collect autoupdates until a AUTOUPDATE_DELAY is reached (since the first autoupdate)
- Added the AUTOUPDATE_DELAY parameter in the settings.py
- moved some autoupdate code to utils/autoupdate
- moved core/websocket to utils/websocket_client_messages
- add the autoupdate in the response (there are some todos left)
- do not send autoupdates on error (4xx, 5xx)
- the client blindly injects the autoupdate in the response
- removed the unused autoupdate on/off feature
- the clients sends now the maxChangeId (instead of maxChangeId+1) on connection
- the server accepts this.
2020-05-27 16:05:27 +02:00
Emanuel Schütze dcf5d5316c
Merge pull request #5384 from FinnStutzenstein/logErrorsOnServer
Log APIExceptions on the server
2020-05-25 14:52:34 +02:00
FinnStutzenstein fba043fedf
Log APIExceptions on the server 2020-05-25 09:16:37 +02:00
Emanuel Schütze 762d1f9912
Merge pull request #5382 from topelrapha/master
Fix tinymce version to 5.2.2
2020-05-23 10:37:34 +02:00
Raphael Topel 60621bf4d0 Fix tinymce version to 5.2.2 2020-05-22 22:19:45 +02:00
FinnStutzenstein bf88cea200
Rewrite projector code to be cache friendly
This speeds up the requests/seconds by a factor of 100
2020-05-22 15:23:54 +02:00
FinnStutzenstein 23842fd496
Synchronize autoupdate code in the client
If autoupdates are too fast, the first one may not be fully executed. Especially when the maxChangeId is not yet updated, the second Autoupdate will trigger a refresh, because for the client it "lay in the future". This can be prevented by synchronizing the autoupdate-handling code with a mutex.
2020-05-22 15:23:53 +02:00
Sean 4ac7b1eb4b
Merge pull request #5380 from FinnStutzenstein/closService
Fix the CLOS service to trigger on CLOS updates
2020-05-20 18:29:11 +02:00
FinnStutzenstein 17049cc0f3 Fix the CLOS service to trigger on CLOS updates
additionally fixed some naming issues
2020-05-20 18:09:50 +02:00
Emanuel Schütze fd026e165f
Merge pull request #5377 from jsangmeister/migrate-deprecated-slides
Adds migration to remove deprecated slides
2020-05-19 14:35:07 +02:00
Joshua Sangmeister e52697ad7e Adds migration to remove deprecated slides 2020-05-19 12:55:47 +02:00
Emanuel Schütze 0c93c44f0d
Merge pull request #5374 from emanuelschuetze/translations-20200514
Updated translations
2020-05-14 20:09:36 +02:00
Emanuel Schütze 4b95398ac1
Merge pull request #5371 from jsangmeister/jitsi-settings
Added jitsi settings to template and readme
2020-05-14 19:59:28 +02:00
Emanuel Schütze 37c3ac5aff Updated translations 2020-05-14 19:52:18 +02:00
Joshua Sangmeister 3f03f27cdb added jitsi settings to template and readme 2020-05-14 19:41:35 +02:00
Emanuel Schütze f694e2355d
Merge pull request #5373 from tsiegleauq/assignment-poll-card-with-table
Show assignment poll result table instad of chart
2020-05-14 19:36:43 +02:00
Sean 3820e09b89 Show assignment poll result table instad of chart
Replaces the assignment result chart with the result table from the
detail view and projector
2020-05-14 16:40:19 +02:00
Emanuel Schütze 1ca3196a75
Merge pull request #5370 from tsiegleauq/tags-for-agenda
Add tags for agenda items
2020-05-14 15:23:55 +02:00
Emanuel Schütze ee6076f168
Merge pull request #5372 from tsiegleauq/unset-gender
Allow unset gender in user form
2020-05-14 15:18:00 +02:00
Sean b6bb1fe767 Add tags for agenda items
Adds tags for agnda items, adds tag filter in agenda list view, server
changes, client relations, adjust agenda csv exporter
2020-05-14 15:07:59 +02:00
Sean 7609a0c3db Allow unset gender in user form
Fixes a bug that prevented the form from unsetting the gender value
2020-05-14 14:53:44 +02:00
Emanuel Schütze b090e46b66
Merge pull request #5359 from tsiegleauq/clean-projector-permissions
Add permission as lookup object
2020-05-14 14:48:37 +02:00
Emanuel Schütze ca039860f7
Merge pull request #5365 from jsangmeister/default-voting-type
Added config for default poll type
2020-05-14 13:02:51 +02:00
Sean fca4154bb5 Add permission as lookup object
Adds a lookup object for a more solid approach to handling permissions.
Permissions are now an actual type rather than just a string.
2020-05-14 10:12:22 +02:00
Emanuel Schütze 621d0f4e1a
Merge pull request #5366 from tsiegleauq/motion-not-switching-state
Change motion cr mode view behavior
2020-05-14 08:25:01 +02:00
Emanuel Schütze d1b6ed8d29
Merge pull request #5367 from tsiegleauq/hide-submitter-in-projector
Hide the submitter-box in motion slide if empty
2020-05-14 08:22:28 +02:00
Emanuel Schütze 8058a4d695
Merge pull request #5369 from FinnStutzenstein/fixProjectorSubtitle
Fix projector subtitles for items without agenda items
2020-05-14 08:20:55 +02:00
Sean 853bc31e21 Change motion cr mode view behavior
if the default cr mode is 'original' nothing really happens
if the default cr mode is 'changed' you will stay in original view after
creating cr's. That's due to the autoupdate limitation. Changing this
would mean that you cannot change the view anymore
if the default cd mode is 'diff' you will switch to diff view after
creating a cr. It seems that the diff view has an automatic fallback to
the original view if no cr exists, perhaps Tobias knows more about that.
If the default cr mode if 'final' you will try to change to
mod-final-version if it exists. If there is no change-reco, you will
fall back to original version.
2020-05-14 07:42:25 +02:00
Joshua Sangmeister fa63ef0307 added config for default poll type 2020-05-14 07:38:14 +02:00
Sean fef3cf41bb Hide the submitter-box in motion slide if empty
Hides the "submitter" text and corresponding boxes if they are empty
2020-05-14 07:37:18 +02:00
FinnStutzenstein 34d85c996c Fix projector subtitles for items without agenda items 2020-05-14 07:32:44 +02:00
Emanuel Schütze b7b27d2e88
Merge pull request #5368 from jsangmeister/fix-flake
Fixed formatting after flake update
2020-05-14 07:31:15 +02:00
Joshua Sangmeister b0bf4990f8 fixed formatting after flake update 2020-05-13 16:16:03 +02:00
Emanuel Schütze 0ee70b7434
Merge pull request #5363 from emanuelschuetze/translation-20200511
Updated translations
2020-05-11 15:45:29 +02:00
Emanuel Schütze 9938a68865 Updated translations 2020-05-11 15:33:58 +02:00
Sean 3e19840b08
Merge pull request #5352 from tsiegleauq/speaker-list-change-detection
Manual cd for list of speakers
2020-05-11 11:19:55 +02:00
Sean 7a31cff612
Merge pull request #5347 from GabrielInTheWorld/statistics
Create statistics of closed list of speakers
2020-05-11 11:07:53 +02:00
GabrielMeyer e7de593b54 Create statistics of closed list of speakers 2020-05-11 07:56:50 +02:00
Sean 602d1c8e7b Manual cd for lost of speakers
Adds better cd for list of speakers
2020-05-08 12:20:15 +02:00
Emanuel Schütze c5dd2ea261
Merge pull request #5348 from CatoTH/toggle-show-all-amendments
Toggle to show all amendments in diff view
2020-05-08 08:50:47 +02:00
Paul Wolf 8796eeeb62
added proxy pass to nginx configuration file
prevent request hostname being localhost:8000 | passthrough server hostname from nginx
2020-05-07 07:42:48 +02:00
Tobias Hößl 25839ea709
Toggle to show all amendments in diff view 2020-05-06 20:28:19 +02:00
Emanuel Schütze ea830f53b0
Merge pull request #5355 from FinnStutzenstein/SamlInfitySpinnerFix
fixed endless spinner on SAML login
2020-05-06 19:20:28 +02:00
FinnStutzenstein c643a233ae
fixed endless spinner on SAML login 2020-05-06 18:23:38 +02:00
Emanuel Schütze 5aa895bda2
Merge pull request #5354 from FinnStutzenstein/fixUnnenessaryBrokenLock
Fix unnecessary and unfunctional lock for production usage with gunicorn
2020-05-06 17:29:28 +02:00
FinnStutzenstein 2910701422
Fix unnecessary and unfunctional lock for production usage with gunicorn 2020-05-06 16:57:50 +02:00
Sean 1e2395c1e6
Merge pull request #5351 from tsiegleauq/ios-touch-callout
Fix poll results under iOS
2020-05-06 15:07:34 +02:00
Sean fede11b59f Fix poll results under iOS
Fixes an issue on iOS devices regarding click-bindungs in structural
loops. iOS devices did not respond to touch inputs in the given view
2020-05-06 14:32:46 +02:00
Emanuel Schütze 77cf3e2785
Merge pull request #5321 from tsiegleauq/projector-indicator-on-agenda
Projector indicator list view tables
2020-05-05 17:50:27 +02:00
Sean 4e624384e7 Projector indicator list view tables
Shows a projector indicator in the list view tables.
Width is 24px, just as small as mat-icon needs.
Works in every list view that allows projector buttons (which should
cover everything where an indicator could be demanded)
2020-05-05 16:08:32 +02:00
Emanuel Schütze f9cd3ebd89
Merge pull request #5304 from CatoTH/change-recommendations-for-amendments
Change recommendations for amendments
2020-05-05 15:46:00 +02:00
Emanuel Schütze 6a6e90067a
Merge pull request #5349 from tsiegleauq/tinymce-link-settings
Set tinymce urls to absolute
2020-05-05 15:41:52 +02:00
Sean 1a653c3fa7 Set tinymce urls to absolute
Potentially fixes a deeper TinyMCE issue that causes TinyMCE to remove
important parts of pasted URLs
2020-05-04 17:19:05 +02:00
Tobias Hößl b51787129b
Change recommendations for amendments 2020-05-02 18:40:59 +02:00
Emanuel Schütze e0069f734a
Merge pull request #5346 from emanuelschuetze/translations-20200430
Updated translations
2020-04-30 19:29:59 +02:00
Emanuel Schütze f415fd0554 Updated translations 2020-04-30 17:43:48 +02:00
Emanuel Schütze c6836ff6c5
Merge pull request #5294 from GabrielInTheWorld/sort-speakers
Changes workflow for sorting speakers
2020-04-30 16:42:52 +02:00
GabrielMeyer 4a24da12da Changes workflow for sorting speakers 2020-04-30 15:58:49 +02:00
Emanuel Schütze 3842f66877
Merge pull request #5330 from tsiegleauq/mark-first-time-speaker
Show first contribution hint in list of speaker
2020-04-30 15:03:55 +02:00
Sean 38ee6bb2f1 Show first contribution hint in list of speaker
Shows a hint in the list of speakers if a speaker contributes for the
first time.
2020-04-30 14:06:37 +02:00
Emanuel Schütze a47285c0ff
Merge pull request #5309 from tsiegleauq/integrate-jitsi-meet-client
Integrate jitsi-meet in OpenSlides
2020-04-30 13:05:12 +02:00
Sean 1439444b2e Integrate jitsi-meet in OpenSlides
- minimal jitsi client in the bottom right of the screen
- can be shown and hidden like a messenger
- allows to mute, unmute, call, stop call
- automatically connects to a conference
- shows a list of users connected to the room
- jitsi iframe is currently hidden
- "open in jitsi meet" link
- only one connection will be opened if using multiple tabs
- JITSI_DOMAIN and JITSI_ROOM_NAME must be present in the settings.py
- config variables in settings
2020-04-30 11:07:32 +02:00
Emanuel Schütze cce76118c3
Merge pull request #5344 from FinnStutzenstein/voteWeight
Removed vote weight from votes_cast
2020-04-30 10:54:28 +02:00
FinnStutzenstein aa1a2cec89
Removed vote weight from votes_cast 2020-04-30 08:10:39 +02:00
Emanuel Schütze 46d0bbd8f5
Merge pull request #5341 from FinnStutzenstein/loadConfigsBeforeModels
Load configs before models
2020-04-27 10:00:53 +02:00
FinnStutzenstein b78372f8a3
Load configs before models 2020-04-27 09:41:23 +02:00
Emanuel Schütze fd9b8b1c5c
Merge pull request #5340 from emanuelschuetze/translations-20200424
Updated German translations.
2020-04-24 14:40:40 +02:00
Emanuel Schütze 7a25a2496d Updated German translations. 2020-04-24 14:28:26 +02:00
Emanuel Schütze ddfe7d0c5a
Merge pull request #5295 from GabrielInTheWorld/duplicate-topics
Duplicates single and multiple items in the agenda
2020-04-24 14:03:44 +02:00
FinnStutzenstein 152401a9a3
Duplicates single and multiple topics in the agenda 2020-04-24 12:34:22 +02:00
Emanuel Schütze 2057150076
Merge pull request #5322 from FinnStutzenstein/delete-all-speakers
delete all speakers of all lists of speakers
2020-04-24 12:25:49 +02:00
Emanuel Schütze cb52347354
Merge pull request #5328 from FinnStutzenstein/projectorSubtitles
Subtitles for projected elements in the projector detail view
2020-04-24 12:12:22 +02:00
Emanuel Schütze 3169e4f30b
Merge pull request #5336 from FinnStutzenstein/fixVoting
Fixed validation of options in asignment polls
2020-04-24 12:10:04 +02:00
FinnStutzenstein 4221351223 Fixed validation of options in asignment polls
Also fixed #5334
2020-04-24 09:31:39 +02:00
Emanuel Schütze 0c6da9799c
Merge pull request #5335 from GabrielInTheWorld/fixTopicPreview
Fixes preview of topics in global search
2020-04-24 09:13:36 +02:00
Emanuel Schütze a71e36c861
Merge pull request #5331 from emanuelschuetze/translations-20200423
Updated German translations
2020-04-24 08:55:39 +02:00
GabrielMeyer 41b9065807 Fixes preview of topics in global search 2020-04-23 23:36:10 +02:00
Emanuel Schütze 527f947143 Updated German translations 2020-04-23 20:35:59 +02:00
FinnStutzenstein c8faa982ac Subtitles for projected elements in the projector detail view 2020-04-23 16:01:18 +02:00
FinnStutzenstein 38486463bc delete all speakers of all lists of speakers 2020-04-23 09:43:04 +02:00
Finn Stutzenstein 6a488eb78e
Merge pull request #5323 from tsiegleauq/linter-prettier-updates
Update linting rules
2020-04-22 17:32:42 +02:00
Emanuel Schütze 0aef3f79ce
Merge pull request #5305 from tsiegleauq/weight-votes
Implement vote weight in client
2020-04-22 17:28:02 +02:00
Sean 97c2299aec Implement vote weight in client
Implements vote weight in client
The user detail page has a new property
change deserialize to parse floats
change "yes"-voting to send "Y" and "0" instead of "1" and "0"
add vote weight to user list, filter, sort
add vote weight to single voting result
votesvalid and votescast respect the individual vote weight
fix parse-poll pipe and null in pdf
2020-04-22 16:54:50 +02:00
Sean e702843f07 Update linting rules
Includes strickt(er) tslint line-length of 120, with an exception
for import statements (prettier does not like these)
2020-04-22 16:24:08 +02:00
Emanuel Schütze 0f3d07f151
Merge pull request #5324 from tsiegleauq/another-round-of-python-travis-fixes
Set version of pytest-django and pytest-asyncio
2020-04-22 16:09:23 +02:00
Sean aa097ee689 Set version of pytest-django and pytest-asyncio
Apparently, updates in these two libraries are not compatible
with out current testing setup
2020-04-22 14:56:25 +02:00
Sean f7a97cf886
Merge pull request #5299 from CatoTH/delete-obsolete-tests
Delete old test files that are reimplemented in Typescript by now
2020-04-21 13:21:16 +02:00
Emanuel Schütze 25f8f42c92
Merge pull request #5320 from sarah-github/sarah-github-typo-settings-rst
typo with brackets in settings.rst for logging config
2020-04-21 12:49:04 +02:00
Sarah 523eb96f9d
typo with brackets 2020-04-20 22:37:01 +02:00
Emanuel Schütze 2c548d2dfb
Merge pull request #5319 from tsiegleauq/fix-admin-self-set-present
Fixes an issue regarding self-set-present
2020-04-20 19:38:11 +02:00
Sean 91d4b3c7af Fixes an issue regarding self-set-present 2020-04-20 19:17:03 +02:00
Emanuel Schütze d210496146
Merge pull request #5293 from GabrielInTheWorld/subline-comment
Adds a third line for comments
2020-04-20 18:09:52 +02:00
Emanuel Schütze 35ce596706
Merge pull request #5317 from tsiegleauq/set_presence_on_userlist
Allow set present on user list if config was set
2020-04-20 17:13:48 +02:00
Tobias Hößl f007e07544
Delete old test files that are reimplemented in Typescript by now 2020-04-20 15:15:00 +02:00
Sean 70aadcdd28
Merge pull request #5307 from GabrielInTheWorld/no-found-button
Adds a 'not found'-button to search-value-selector
2020-04-20 14:52:46 +02:00
Sean 9ffbb39e95 Allow set present on user list if config was set
Depending on "selt self presence" config, allow users to set
themselves as present on the user list
2020-04-20 13:46:14 +02:00
GabrielMeyer 170aa1c8f0 Adds a third line for comments 2020-04-20 07:46:37 +02:00
jsangmeister ad4ed3443a
Merge pull request #5316 from jsangmeister/voting-autoupdate-fix
Fixes the voting autoupdate bug
2020-04-17 16:58:50 +02:00
Joshua Sangmeister 42fbe93314 fixes the voting autoupdate bug 2020-04-17 16:38:38 +02:00
Emanuel Schütze 6cdf9a5582
Merge pull request #5315 from emanuelschuetze/prepare-3.2
Prepare new 3.2 release
2020-04-16 13:55:24 +02:00
Emanuel Schütze 75ebf5bc77 Prepare new 3.2 release 2020-04-16 13:41:56 +02:00
Emanuel Schütze c26ef8c0bb
Merge pull request #5314 from jsangmeister/permissiveHtmlValidation
Adds more permissive html validation for topics and welcome page
2020-04-16 10:57:43 +02:00
Joshua Sangmeister 6eae497abe adds more permissive html validation 2020-04-16 10:26:48 +02:00
Finn Stutzenstein 1570b5b806
Merge pull request #5313 from jsangmeister/fix-user-import
Fixed user import
2020-04-15 13:44:30 +02:00
Emanuel Schütze 537eeadce4
Merge pull request #5312 from jsangmeister/adjustSettingsReadme
Add ENABLE_ELECTRONIC_VOTING to settings readme
2020-04-15 11:09:48 +02:00
Joshua Sangmeister 96ee1c0af3 fixed user import 2020-04-15 10:27:07 +02:00
Joshua Sangmeister 99416e3043 added ENABLE_ELECTRONIC_VOTING to settings readme 2020-04-15 10:15:19 +02:00
Emanuel Schütze 0f8167e39c
Merge pull request #5311 from jsangmeister/fixPseudoanonymize
Fix pseudoanonymize on client
2020-04-15 09:54:21 +02:00
Joshua Sangmeister 9864ff3847 fixed pseudoanonymize on client 2020-04-15 09:42:22 +02:00
Emanuel Schütze a7518ed5b6
Merge pull request #5310 from tsiegleauq/better-category-sorting
Sort motions in categories by inner weight (again)
2020-04-15 09:25:19 +02:00
Sean 5b7bbfd0bb Sort motions in categories by inner weight (again)
Fixes sorting categories by inner weight
2020-04-14 15:01:42 +02:00
Emanuel Schütze b7566fcc69
Merge pull request #5308 from tsiegleauq/sort-categories-by-weight
Sort motion list by category weight
2020-04-08 22:35:31 +02:00
Sean 82c6929a8d Sort motion list by category weight
Sorts the motion list by the weight of the category
rather than by name
2020-04-08 22:22:02 +02:00
GabrielMeyer 35a67017a3 Adds a 'not found'-button to search-value-selector 2020-04-08 11:48:18 +02:00
Finn Stutzenstein 4841343c02
Merge pull request #5303 from FinnStutzenstein/fixVoting
Cleanup for #5300
2020-04-07 10:07:07 +02:00
FinnStutzenstein 7a97aa1b79 Cleanup for #5300 2020-04-07 09:53:16 +02:00
Emanuel Schütze 12bc926b44
Merge pull request #5302 from emanuelschuetze/translations-20200407
Updated translations.
2020-04-07 09:40:02 +02:00
Emanuel Schütze 53b4b1c1f9 Updated translations. 2020-04-07 07:56:51 +02:00
Emanuel Schütze cc372cfba5
Merge pull request #5300 from FinnStutzenstein/fixVoting
Added vote weight and fixed named voting
2020-04-07 07:47:12 +02:00
Joshua Sangmeister b7b8620153 removed race condition & cleanup 2020-04-07 07:27:54 +02:00
FinnStutzenstein 7882ea1a25 Added vote weight and fixed named voting 2020-04-07 07:27:54 +02:00
Sean 04a7ce22fd
Merge pull request #5297 from tsiegleauq/esr-cd
Fix malfunctions in Firefox ESR
2020-04-06 23:34:32 +02:00
Sean 820a47123a Fix malfunctions in Firefox ESR
Various cryptic issues that had no usefull debug output.

ngx-translates "translate" directive was causing Firefox ESR
to jump in the "drainMicroTaskQueue" infinite recursion when
using the directive in the same component as nGrid.

Therefore, I changed all uses of the translate directive
to the pipe (arround 700 or so, regex is my best friend now)

open todo: adjust the linter that we may never use the translate
directive again.

There was another cryptic issue with the current version
of exceljs which was also causing firefox to go crash
when loading a workbook object. It was sufficient to
have any Workbook() function declared in a module to cause
firefox to crash.
2020-04-06 20:36:24 +02:00
Emanuel Schütze 42af962248
Merge pull request #5301 from tsiegleauq/new-extract-function
Update translation extractor
2020-04-06 17:52:16 +02:00
Sean 7b5f2648af Update translation extractor 2020-04-06 17:18:04 +02:00
Sean a1e2c49815
Merge pull request #5234 from tsiegleauq/angular9
Migrate to angular 9
2020-04-01 18:12:34 +02:00
Sean Engelhardt e1acf6e9d6 Update to Anulgar 9
Updates Angular to version 9.1
Updates most-to-all npm components
Removes deprecated components and npm commands
Updates travis node version
Adjust the whole code base to angular 9 standard
Increase TypeScipt version to 3.8
2020-04-01 14:50:15 +02:00
Finn Stutzenstein 83d57e9da7
Merge pull request #5290 from FinnStutzenstein/redisWaitForReplication
Redis: Wait for replication on writes
2020-04-01 13:32:11 +02:00
FinnStutzenstein bb2f958eb5 Redis: Wait for replication on writes
Since channels_redis does not support dedicated read-redis instances, the
autoupdate message may be received before the data was replicated. All workers
read the autoupdate message from the write host, so there is a race between
getting this message and a finished replication. For large payloads, the
replication is slower in the most cases (even more in a distributed setup, where
the master and replica are on different nodes). The easy way is to wait for
replication. But there is one difficulty: The number of replicas has to be
known. There is a new settings-variable "AMOUNT_REPLICAS" which defaults to 1.
It needs to be set correctly! If it is too high, every autoupdate will be
delayed by 1 seconds because of a timeout witing for non-existent replicas. If
it is too low, some autoupdates may be wrong (and not detectable by the client!)
becuase of reading from non-synchronised relicas.

The other possibility is to fork channel_redis and add the feature of a
read-only redis. This ould help, because on a single redis instance all commands
are ordered: First, the data is synced, then the autoupdate message. Attention:
This means, if redis-replicas are scaled up, one must make sure to read from the
same instance. I think this is not possible in the way how dockers overlay
networks work. The only way would be to open one connection and reuse the
connection from channels_redis in OpenSlides. This would mean a heavy
integration of channels_redis (meaning including the source code in our repo).

For the first fix, this one is easy and should work.
2020-04-01 13:09:48 +02:00
Emanuel Schütze 7b0a2d8ec2
Merge pull request #5291 from FinnStutzenstein/updateTwisted
Update Twisted
2020-04-01 10:46:17 +02:00
FinnStutzenstein b2d05f81fe Update Twisted 2020-04-01 10:33:18 +02:00
Emanuel Schütze 4419e76223
Merge pull request #5289 from FinnStutzenstein/fixMissingMigrations
Added missing migrations
2020-03-30 11:16:38 +02:00
Emanuel Schütze 1e3c83babc
Merge pull request #5288 from emanuelschuetze/translations-20200330
Updated Translations
2020-03-30 11:08:32 +02:00
FinnStutzenstein 3be28ec50a Added missing migrations 2020-03-30 10:58:42 +02:00
Emanuel Schütze baa1787189 Updated Translations 2020-03-30 10:57:10 +02:00
Emanuel Schütze 8119507b8a
Merge pull request #5283 from tsiegleauq/user-active-toggle
Add set present toggle in user menu
2020-03-30 10:14:27 +02:00
Sean 39ccfe3147 Add set present toggle in user menu
adds a "is present" toggle to the user menu
Refactor user menu into own component
Add a config variable to determine if the user is allowed
to set themselve as present
2020-03-30 09:57:57 +02:00
Emanuel Schütze 106816a733
Merge pull request #5286 from emanuelschuetze/translations-20200326
Updated translations.
2020-03-26 18:42:44 +01:00
Emanuel Schütze c257baa14b Updated translations. 2020-03-26 18:21:36 +01:00
Emanuel Schütze 04c625b3d5
Merge pull request #5282 from tsiegleauq/dynamic-bar-chart-aspect-ratio
Define chart height
2020-03-26 17:58:48 +01:00
Sean d646691961 Define chart height
Bar chart height will be calculated by the number of labels to show
Circle chart height should scale with their container
2020-03-26 17:39:47 +01:00
Emanuel Schütze aaea4ec2e9
Merge pull request #5284 from tsiegleauq/clickable-motion-charts
Remove tooltips for charts
2020-03-26 17:19:19 +01:00
Emanuel Schütze 5b878f4814
Merge pull request #5285 from tsiegleauq/untranslated-comment-field
Fix untranslateable motion comment section dialog
2020-03-26 17:17:01 +01:00
Sean 5bdbe4778a Remove tooltips for charts
Removes all hover-tooltips for charts
Add detail link to motion poll chart
2020-03-26 17:04:17 +01:00
Sean fbff4de431 Fix untranslateable motion comment section dialog
Missing translation tags in comment section dialog
2020-03-26 16:37:47 +01:00
Sean af6c5faac8
Merge pull request #5281 from tsiegleauq/poll-list-filter
Filter only evoting polls, change filter options
2020-03-26 12:27:33 +01:00
Sean 14de67a09d Filter only evoting polls, change filter options
Pre filters all analog polls from poll list.
Creates a filter to find thouse with open polls for the user
2020-03-26 11:54:05 +01:00
Emanuel Schütze 6f7c6036c2
Merge pull request #5280 from tsiegleauq/workflow-manager-scrolling
Enhance workflow matrix for small screens
2020-03-25 15:26:18 +01:00
Emanuel Schütze 19af02a315
Merge pull request #5278 from tsiegleauq/tinymce-in-settings-page
Manual change detection for config list
2020-03-25 15:21:41 +01:00
Emanuel Schütze d50899c407
Merge pull request #5279 from tsiegleauq/less-verbose-user-count
Remove counting of IndexedDB and LocalStore
2020-03-25 13:10:00 +01:00
Sean 73fc936306 Enhance workflow matrix for small screens
makes scrolling horizontally in the workflow manager easier.
The offline banner and the voting banner will lead to unwanted results.
2020-03-25 13:01:25 +01:00
Sean c2406fcc03 Remove counting of IndexedDB and LocalStore
Mainly simplify the user counting component.

It seems that counting IndexedDB has no value anymore,
since even Firefox ESR uses IndexedDB just fine and the LS fallback
proves to be reliable enough
2020-03-25 12:18:57 +01:00
Sean 557824f5f1 Manual change detection for config list
Adds manual change detection to the config page.
Behaves better regarding "changed after checked" errors
2020-03-25 11:22:54 +01:00
Emanuel Schütze 91be76a263
Merge pull request #5276 from tsiegleauq/changeable-created-polls
Remove pseudo-disabled values in poll form
2020-03-24 16:01:17 +01:00
Emanuel Schütze eadc09dc56
Merge pull request #5271 from tsiegleauq/reset-workflow-reco
Allow empty input values in recommendation
2020-03-24 15:58:43 +01:00
Emanuel Schütze c43e180494
Merge pull request #5277 from FinnStutzenstein/removeCheckUpdate
Remove Check update for others feature
2020-03-24 15:56:50 +01:00
Sean 6fddddd9f4 Allow empty input values in recommendation
Allows the client to send empty values as workflow
recommendation label in the workflow detail view.
This is required to remove a recommendation from a
workflow
2020-03-24 15:44:46 +01:00
FinnStutzenstein cf50295ca4 Remove Check update for others feature 2020-03-24 15:25:47 +01:00
Sean 7af2f70494 Remove pseudo-disabled values in poll form
Removed the previously disabled-looking poll options from the poll form
component.
I figguered that it would be more intuitive and closer to my
expectations if I could change the poll values even if the poll was
already created.
2020-03-24 14:22:59 +01:00
Emanuel Schütze cd3435064c
Merge pull request #5275 from FinnStutzenstein/default169projector
Changed the default projector aspect ratio to 16:9 on initial databas…
2020-03-24 12:44:05 +01:00
FinnStutzenstein 123df7660f Changed the default projector aspect ratio to 16:9 on initial database creation 2020-03-24 12:09:11 +01:00
Sean 2fb372ead9
Merge pull request #5274 from tsiegleauq/untranslated-default-projector
Show translated values in projector preview
2020-03-23 17:57:09 +01:00
Sean 7d86f62e2d
Merge pull request #5273 from tsiegleauq/new-projector-reference
Create new projectors with clos reference
2020-03-23 17:31:13 +01:00
Sean d92622410f Show translated values in projector preview
Shows the translated value of "default projector"
in projector preview component.
I reomved a observable that I suppose was outdated
2020-03-23 17:28:09 +01:00
Sean 99c3afb417 Create new projectors with clos reference
New projectors will be created on a reference to the current list of
of speakers
2020-03-23 16:57:29 +01:00
Emanuel Schütze 23a105bdb8
Merge pull request #5272 from tsiegleauq/los-select-speaker-css
Full width for LOS search user
2020-03-23 16:57:01 +01:00
Sean bf0eadebb7 Full width for LOS search user
Puts the List-Of-Speakers search user section to full width
2020-03-23 16:32:48 +01:00
Sean fe71322199
Merge pull request #5270 from tsiegleauq/hotkey-for-poll-dialogs
Fix Shift-Enter to save poll
2020-03-23 14:45:54 +01:00
Sean 5bf3dfadff Fix Shift-Enter to save poll
Fixes a bug which caused the event listener not to register
in BasePollDialog

Also hot fixes a bug in babel
2020-03-23 14:26:38 +01:00
Emanuel Schütze 5617b02804
Merge pull request #5266 from emanuelschuetze/translations-20200320
Updated translations
2020-03-20 21:00:10 +01:00
Emanuel Schütze 5a6d2d2e42 Updated translations 2020-03-20 17:19:12 +01:00
Sean 661fd55c67
Merge pull request #5265 from tsiegleauq/assignment-poll-slide-rework
Rework assignment poll slide
2020-03-20 17:14:44 +01:00
Sean 072ec937a1 Rework assignment poll slide
Reworked assignment poll slide
and refactored the assignment poll detail table
into an own component
2020-03-20 16:53:31 +01:00
Emanuel Schütze b873dc156b
Merge pull request #5264 from tsiegleauq/bar-chart-rework
Rework Chart component
2020-03-20 13:31:20 +01:00
Emanuel Schütze 4acadd33ca
Merge pull request #5262 from FinnStutzenstein/catchErrorOnInvalidCache
Catch error on invalid cache data
2020-03-20 13:24:53 +01:00
Sean f0e396b3a4 Rework Chart component
Cleans up the chart component
Speed up the rendering using async pipe instead of passing obserbables
Thiner bar-charts.
Fixes some bugs, some bugs are still present.
2020-03-20 10:28:59 +01:00
FinnStutzenstein 73eff81edd Catch error on invalid cache data 2020-03-19 14:00:56 +01:00
Emanuel Schütze 54dd97399e
Merge pull request #5260 from tsiegleauq/various-voting-issues
Various small fixes
2020-03-17 21:53:40 +01:00
Sean ee07e8f0ce Various small fixes
- Fix an issue in motion PDF which affected motion result percent
  values
- Fix an issue where the voting result bar chart hat a chance to show
  "null"
- Change the available votes display to count down instead of up
- Add the correct button class to the global abstain button
- Add some translatable strings
2020-03-17 21:33:46 +01:00
Sean d12e052030
Merge pull request #5259 from tsiegleauq/clean-usage-of-pollservice
Refactor usage of PollService
2020-03-17 18:39:18 +01:00
Sean 0ab4532ac8 Refactor usage of PollService
prevents the direct use of the abstract
"PollService".
2020-03-17 18:21:13 +01:00
Finn Stutzenstein 58483d7024
Merge pull request #5255 from OpenSlides/development
Voting 🎉
2020-03-17 07:48:00 +01:00
Sean 3c9f6ed278 Some overall improvements
Common:
	delete unused motion poll list

Poll Create form:
	Fix ugly multi line mat hints
	(workaround, see https://github.com/angular/components/issues/5227 )

Poll List:
	Fix too tiny column size
	user_has_voted_valid (ceck icon) was not shown

Motion Poll Card:
	Enhance subtitle layout (type + state)

Assignment Poll Card:
	Open warning after clicking the hint icon

Assignment Poll Chart:
	Show Absolute values and percents in chart label

Assignment Detail:
	Add new ballot button with plus icon instead of chart icon
2020-03-17 07:24:50 +01:00
FinnStutzenstein 64f2720b1a Last changes and cleanup some todos 2020-03-17 07:24:50 +01:00
Emanuel Schütze d15c9892ed Updated translation strings and German translation. 2020-03-17 07:24:49 +01:00
Sean Engelhardt ee4c6aa0bf Even more voting refinement
Various additional refinements for a more well rounded
voting experience
2020-03-17 07:24:49 +01:00
FinnStutzenstein a05662a0f8 Show global votes in the single votes table 2020-03-17 07:24:49 +01:00
Sean Engelhardt 29a9a09bc6 Motion poll detail als slide
Refactor the code to use the motion poll detail als slide component
2020-03-17 07:24:49 +01:00
Sean Engelhardt 3c36441967 Add global no and abstain to form
Minur UI changes
Minor Chart enhancements
Server Changes
2020-03-17 07:24:48 +01:00
Sean Engelhardt 8fe5a0c9f4 Rework assignment voting
- Remove "assignments.can_manage_polls" permission
- Let the client handle some user errors
- Add a send button to manually submit polls
- Show a hint that the user already submitted a vote
  - will not (and should not) work for non-nominal voting
- submitting a vote cannot be changed anymore
  - user will have to confirm sending
- enable deselecting YNA-votings
- nomainal voting will behace the same as non nominal voting
- submitting empty votes should be possible

Perhaps server side adjustments might still be required
2020-03-17 07:24:48 +01:00
Sean Engelhardt 61b7731073 Enhance charts and tables for assignments
Also some various improvements
2020-03-17 07:24:48 +01:00
FinnStutzenstein e2feeb4b65 Fixed pseudoanonymous voting for motions 2020-03-17 07:24:47 +01:00
Sean Engelhardt 53b9ce73f2 Enhance table layouts
Enhance the result table layout for assignments
2020-03-17 07:24:47 +01:00
Joshua Sangmeister 9d7028ea5f now filters percent bases correctly 2020-03-17 07:24:47 +01:00
Joshua Sangmeister 72678770bb table update on pseudoanonymize, view base classes for votes and
options, renaming for assignment percent bases
2020-03-17 07:24:46 +01:00
Joshua Sangmeister 82c8ade0ba adds all user that voted to required users for single votes table 2020-03-17 07:24:46 +01:00
Emanuel Schütze 2d13519c35 Updated voting strings for translation 2020-03-17 07:24:46 +01:00
Joshua Sangmeister e72bcc1eaf removed default group from 'entitled to vote' selection 2020-03-17 07:24:46 +01:00
Sean Engelhardt 97a5bb4aa6 Cleanup Voting, enhance UI and UX
removed certain unnecesary fields
cleaned up a lot of code
redone some of the UI
some database and server adjustments
2020-03-17 07:24:45 +01:00
Joshua Sangmeister 7598fc5367 Fixed the numbering of assignment candidates for projector and PDF 2020-03-17 07:24:45 +01:00
Joshua Sangmeister b48ca8c434 added chart projection for polls 2020-03-17 07:24:45 +01:00
Sean Engelhardt 6ba0d0c5e6 Client side changes 2020-03-17 07:24:44 +01:00
FinnStutzenstein 0b37c5a857 WIP: Partial requests 2020-03-17 07:24:44 +01:00
Joshua Sangmeister d4599a435b added virtual scrolling for single votes tables 2020-03-17 07:24:44 +01:00
Sean Engelhardt 93dc78c7d6 Result PDF for Voting
- Add result PDF for Motion and Assignments
- Add "getPercentBase" for Assignment
2020-03-17 07:24:44 +01:00
Sean Engelhardt 6044c63c28 Enhance Assignment Voting
- repaired the PDF Service for ballots
- fixed some permission errors
- analog voting has no "started" option anymore
- more-link as button
- named voting has a progress bar
- Shows the poll type for eVoting
- Moves and declutters meta info
- Enhance the grid and the layout in detail view
- declutter and enhance the dot-menus
- some other layout changes
- remove breadcrumbs in assignment detail
- other cleanups refinements
- Voting in Assignment over instead of forms
(requires more server changes)
2020-03-17 07:24:43 +01:00
Sean 524a97cdcc Enhance voting
- cleaned up a lot of code
- removed required majotiry from forms
- renamed verbose "Majority method" to "Required majority"
- poll-progress-bar only counts present user
- enhanced motion poll tile chart layout
- removed PercentBase.Votes
- added pollPercentBase pipe
- Show the voting percent next to chart in motion detail
- change the head bar to "Voting is open"
  and "Ballot is open"
- merged the voting configs to their corresponding config-categories
- re-add ballot paper configs
- Add "more" button to motion polls
- Adjusted the motion results table
  - Hide entries without information
  - Show icons for Y N A
  - Show percentage next to Y N A
2020-03-17 07:24:43 +01:00
GabrielMeyer 6c1317e25f Fixes labelling for charts 2020-03-17 07:24:43 +01:00
GabrielMeyer 294b75c320 Replaces the mat-table with a classic table 2020-03-17 07:24:42 +01:00
GabrielMeyer 09b0d19de0 Fixes permissions for assignments
- There were some fields that user could see/click/handle, although the user has not the correct permission for the action
2020-03-17 07:24:42 +01:00
Joshua Sangmeister df1047fc76 various improvements for polls 2020-03-17 07:24:42 +01:00
Joshua Sangmeister bc54a6eb46 improved 'votes' pollmethod 2020-03-17 07:24:42 +01:00
Joshua Sangmeister 1de73d5701 improved shared poll list 2020-03-17 07:24:41 +01:00
GabrielMeyer a0c3a28456 Adds a chart for assignment-poll-detail 2020-03-17 07:24:41 +01:00
GabrielMeyer c46369c6a7 Reworks the banner showing if there are polls
- Makes it higher on mobilephones
- Changes title, if there is only one poll
2020-03-17 07:24:41 +01:00
Joshua Sangmeister b16afaa285 number poll candidates depending on setting 2020-03-17 07:24:40 +01:00
FinnStutzenstein e2585fb757 Projector for polls: Server, client structure and data modeling 2020-03-17 07:24:40 +01:00
Sean Engelhardt 84a39ccb62 More voting UI improvements
For Motion poll:
- Overworked how motion poll chart displays the legend
- Added the vote counter to the motion detail
- Added a progress bar to the vote counter
- Fixed some perm errors with the chart
- Show a "Singe Votes" link as button for published named polls
- Replace the edit-button with a dot-menu
  - Having project, Edit, PDF and Delete

For Motion Poll detail:
- enhance search panel
- Remove the breadcrumbs
- Remove the vote counter
- Enhanced the single-vote grid, table and filter bar
- Enhance how the poll state enum was checkend

For the Motion Poll Create/Update Form:
- Remove the selection of poll-methode (whenever possible)
- only show "publish imediately" during creation
2020-03-17 07:24:40 +01:00
Joshua Sangmeister 682db96b7c added vote per user table and progress for polls
added update for options after stopping a poll
2020-03-17 07:24:39 +01:00
Sean 604df9d48b Enhance voting ux 2020-03-17 07:24:39 +01:00
FinnStutzenstein 7ab5346198 disable caching for reverse relations 2020-03-17 07:24:39 +01:00
Joshua Sangmeister e67ca77ad1 default motion poll method set, changed permission from 'can_manage_metadata' to 'can_manage' 2020-03-17 07:24:39 +01:00
GabrielMeyer fff1f15b6c Polls for motions and assignments
- Adds charts to assignments
- Creates base-classes for polls
2020-03-17 07:24:38 +01:00
GabrielMeyer 96aa3b0084 Adds the chart and dialog for analog voting 2020-03-17 07:24:38 +01:00
jsangmeister 72ff1b1f09 api changes to allow some edits on finished polls 2020-03-17 07:24:38 +01:00
FinnStutzenstein fafb81daca Fix assignment access permissions
Also improves unnecessary history-savings of users in the list of speakers
2020-03-17 07:24:37 +01:00
FinnStutzenstein b50cf42543 Prevent stopping an analog poll
Fixed too much logging with the new autoupdate bundling
2020-03-17 07:24:37 +01:00
jsangmeister 90b04366b5 added option to number poll cadidates 2020-03-17 07:24:37 +01:00
GabrielMeyer 8d77c0495b Initial polling 2020-03-17 07:24:36 +01:00
jsangmeister 1b761d31c0 added tests for user creation and try-catch for a probably race-based IntegrityError 2020-03-17 07:24:36 +01:00
jsangmeister 09ef3c5071 add settings variable ENABLE_ELECTRONIC_VOTING 2020-03-17 07:24:36 +01:00
FinnStutzenstein 046a152ec5 generate less queries in the autoupdate system 2020-03-17 07:24:36 +01:00
jsangmeister 6605934a33 added count query decorator 2020-03-17 07:24:35 +01:00
FinnStutzenstein 1246dd54ad majorities in polls 2020-03-17 07:24:35 +01:00
jsangmeister 5fa8341614 added testing for named and pseudoanonymous assignment voting
added queries count tests for assignment and motion polls and votes
2020-03-17 07:24:35 +01:00
FinnStutzenstein ce171980e8 Relations in the client 2020-03-17 07:24:34 +01:00
FinnStutzenstein ced40cab74 Initial work for supporting voting 2020-03-17 07:24:34 +01:00
Emanuel Schütze 4d4697eee0
Merge pull request #5246 from jsangmeister/motion-block-check
Added an indicator if all motions of a block are finished
2020-03-12 21:24:01 +01:00
Joshua Sangmeister aa46922c8b added an indicator if all motions of a block are finished 2020-03-12 16:21:41 +01:00
Emanuel Schütze ec17376e8e
Merge pull request #5245 from jsangmeister/amendment-navigation
Added navigation between amendments when not shown in main list
2020-03-12 15:39:26 +01:00
Emanuel Schütze 35d9fd9d8e
Merge pull request #5244 from jsangmeister/fix-final-print-template
Fix the error when creating a final print template after a title change recommendation
2020-03-12 15:14:58 +01:00
Joshua Sangmeister 7acf2157fa added navigation between amendments when not shown in main list 2020-03-12 11:53:29 +01:00
Joshua Sangmeister 70fc5a69ab fix the error when creating a final print template after a title change recommendation 2020-03-12 09:39:56 +01:00
Emanuel Schütze 3ad8944b9c
Merge pull request #5230 from FinnStutzenstein/exportMediafiles
Export mediafiles to the media service
2020-03-06 10:56:53 +01:00
Emanuel Schütze 847482bb5f
Merge pull request #5228 from GabrielInTheWorld/updatingDarkTheme
Updates dark-theme
2020-02-27 18:40:17 +01:00
FinnStutzenstein 219103129d Export mediafiles to the media service 2020-02-26 15:15:45 +01:00
GabrielMeyer 13de88c136 Updates dark-theme 2020-02-26 13:39:07 +01:00
Sean 98146a29c7
Merge pull request #5227 from FinnStutzenstein/projectorLoadChildrenCallback
Use LoadChildrenCallback instead of deprecated NgModuleFactoryLoader
2020-02-25 10:50:19 +01:00
Finn Stutzenstein 758e059f9b
Merge pull request #5153 from FinnStutzenstein/mediafilesInPostgresql
External postgres as mediafile store
2020-02-20 08:01:39 +01:00
FinnStutzenstein 7204d59d66 [WIP] External postgres as mediafile store 2020-02-17 14:38:32 +01:00
FinnStutzenstein 76bd184ff4 Use LoadChildrenCallback instead of deprecated NgModuleFactoryLoader 2020-02-17 14:38:02 +01:00
Emanuel Schütze fbe5ea2056
Merge pull request #5209 from tsiegleauq/hide-create-final-version-button
Hide print-template button
2020-01-30 14:18:31 +01:00
Sean Engelhardt 2236f63fe9 Hide print-template button
Hides the "create final print template" button if
a print template exists.
Should be more user friendly and less dangerous to use.
2020-01-30 13:13:52 +01:00
Emanuel Schütze ec79f70648
Merge pull request #5206 from emanuelschuetze/translation202001
Updated German translation
2020-01-30 09:48:46 +01:00
Emanuel Schütze 0267b0cb42 Updated German translation 2020-01-30 09:35:57 +01:00
Emanuel Schütze 2ac01a5ea3
Merge pull request #5193 from tsiegleauq/final-version-without-reco
Allow final versions without change reco
2020-01-30 09:31:36 +01:00
Sean a51720e18b
Merge pull request #5191 from tsiegleauq/pdf-catalog-optional-page-breaks
Add option to export motions with pagebreaks
2020-01-29 16:33:37 +01:00
Sean Engelhardt 27e8301131 Allow final versions without change reco
Alters the creation process of final versions
in a way one can create them independantly
of change recomendations.
2020-01-29 16:28:36 +01:00
Sean Engelhardt 407a430419 Add option to export motions with pagebreaks
Adds an option to the PDF motion list exporter
that allows users to explicitly enforce or prevent
page breaks before a new motion was printed.

The option is enabled by default
2020-01-29 15:18:56 +01:00
Emanuel Schütze a6bdaedff1
Merge pull request #5192 from tsiegleauq/reco-and-state-in-pdf-toc
Show final state in Motion PDF toc
2020-01-29 15:09:50 +01:00
Sean 59795f32e3
Merge pull request #5195 from tsiegleauq/fix-flat-map-function
Fix flat map
2020-01-29 15:03:49 +01:00
Sean Engelhardt a161bca028 Show final state in Motion PDF toc
Renamed the option
"Show submitters and recommendation in table of contents"
to
"Show submitters and recommendation/state in table of contents"
If the option is selected, the PDF-TOC for motions will print
the name of the current state (if final) rather than the
current recomendation
2020-01-29 14:28:56 +01:00
Sean Engelhardt 6f114d0072 Fix flat map
Fixes a bug which was breaking flatMap
2020-01-29 12:47:42 +01:00
Sean 8012bfbfc0
Merge pull request #5187 from tsiegleauq/ngrid-updates
Update ngrid, redefine prototypes of datatypes
2020-01-23 15:10:18 +01:00
Sean d311042806
Merge pull request #5173 from tsiegleauq/amendment-wizzard-for-amendments
Use amendmend wizzard for amendments
2020-01-23 13:06:54 +01:00
Sean Engelhardt faf8004280 Use amendmend wizzard for amendments
Allows paragraph based amendments for other paragraph based
amendments.
Amendments to amendments will be amended to the main motion,
but will contain all the changes to the amendments they were
refering to

solves #5171
2020-01-23 12:46:19 +01:00
Sean Engelhardt c2ad39a2c5 Update ngrid, redefine prototypes of datatypes
Updates ngrid to the latest version
Use Object.defineProperty instead of
Number.prototype to safer extend native datatypes
2020-01-23 12:38:46 +01:00
Emanuel Schütze 7a23139f5e
Merge pull request #5168 from FinnStutzenstein/validateHtml
Validate Config HTML
2020-01-08 13:36:17 +01:00
Emanuel Schütze b9e40717de
Merge pull request #5172 from tsiegleauq/fix-group-creation
Fix group creation
2020-01-08 13:34:04 +01:00
Sean Engelhardt 5f8e64140a Fix group creation
Fixes an error that was preventing groups from being created.
Groups did require a list of permissions which is now optional
2020-01-08 10:46:11 +01:00
Emanuel Schütze a2d561f667
Merge pull request #5167 from emanuelschuetze/createopenslidesusercommand
Added optional argument '--email' for createopenslidesuser command.
2020-01-07 08:55:32 +01:00
Emanuel Schütze b3c98dd207 Added optional argument '--email' for createopenslidesuser command. 2020-01-05 15:17:09 +01:00
FinnStutzenstein a35fa105ed Validate Config HTML 2020-01-04 16:58:11 +01:00
825 changed files with 29041 additions and 12362 deletions

8
.gitignore vendored
View File

@ -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

View File

@ -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"

View File

@ -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].

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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
- [@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
- [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
- [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
- [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
- [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
- [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
- [@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@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
- [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-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.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
- [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
- [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.8.3](https://github.com/Microsoft/TypeScript), License: Apache-2.0

View File

@ -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": [{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}],
"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": [{
"type": "initial",
"maximumWarning": "5mb",
"maximumError": "10mb"
}]
"budgets": [
{
"type": "initial",
"maximumWarning": "5mb",
"maximumError": "10mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
]
},
"es5": {
"budgets": [
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
],
"tsConfig": "./tsconfig-es5.app.json"
}
}

View File

@ -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"
}
}

View File

@ -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 }
]
},
{

View File

@ -1,3 +1,4 @@
.content {
flex: 1;
height: 100vh;
}

View File

@ -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();

View File

@ -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,45 +114,84 @@ 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 {
let string = '';
const iterations = Math.min(this.length, 3);
private overloadArrayFunctions(): void {
Object.defineProperty(Array.prototype, 'toString', {
value: function (): string {
let string = '';
const iterations = Math.min(this.length, 3);
for (let i = 0; i <= iterations; i++) {
if (i < iterations) {
string += this[i];
}
for (let i = 0; i <= iterations; i++) {
if (i < iterations) {
string += this[i];
}
if (i < iterations - 1) {
string += ', ';
} else if (i === iterations && this.length > iterations) {
string += ', ...';
if (i < iterations - 1) {
string += ', ';
} else if (i === iterations && this.length > iterations) {
string += ', ...';
}
}
}
return string;
};
return string;
},
enumerable: false
});
Object.defineProperty(Array.prototype, 'flatMap', {
value: function (o: any): any[] {
const concatFunction = (x: any, y: any[]) => x.concat(y);
const flatMapLogic = (f: any, xs: any) => xs.map(f).reduce(concatFunction, []);
return flatMapLogic(o, this);
},
enumerable: false
});
Object.defineProperty(Array.prototype, 'intersect', {
value: function <T>(other: T[]): T[] {
let a = this;
let b = other;
// indexOf to loop over shorter
if (b.length > a.length) {
[a, b] = [b, a];
}
return a.filter(e => b.indexOf(e) > -1);
},
enumerable: false
});
Object.defineProperty(Array.prototype, 'mapToObject', {
value: function <T>(f: (item: T) => { [key: string]: any }): { [key: string]: any } {
return this.reduce((aggr, item) => {
const res = f(item);
for (const key in res) {
if (res.hasOwnProperty(key)) {
aggr[key] = res[key];
}
}
return aggr;
}, {});
},
enumerable: false
});
}
/**
* Adds an implementation of flatMap.
* 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 {
return ((this % n) + n) % n;
};
Object.defineProperty(Number.prototype, 'modulo', {
value: function (n: number): number {
return ((this % n) + n) % n;
},
enumerable: false
});
}
}

View File

@ -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]

View File

@ -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) {

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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,27 +134,41 @@ export class AutoupdateService {
// Normal autoupdate
if (autoupdate.from_change_id <= maxChangeId + 1 && autoupdate.to_change_id > maxChangeId) {
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS);
// Delete the removed objects from the DataStore
for (const collection of Object.keys(autoupdate.deleted)) {
await this.DS.remove(collection, autoupdate.deleted[collection]);
}
// Add the objects to the DataStore.
for (const collection of Object.keys(autoupdate.changed)) {
await this.DS.add(this.mapObjectsToBaseModels(collection, autoupdate.changed[collection]));
}
await this.DS.flushToStorage(autoupdate.to_change_id);
this.DSUpdateManager.commit(updateSlot, autoupdate.to_change_id);
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
for (const collection of Object.keys(autoupdate.deleted)) {
await this.DS.remove(collection, autoupdate.deleted[collection]);
}
// Add the objects to the DataStore.
for (const collection of Object.keys(autoupdate.changed)) {
await this.DS.add(this.mapObjectsToBaseModels(collection, autoupdate.changed[collection]));
}
if (flush) {
await this.DS.flushToStorage(autoupdate.to_change_id);
}
this.DSUpdateManager.commit(updateSlot, autoupdate.to_change_id);
}
/**
* Creates baseModels for each plain object. If the collection is not registered,
* A console error will be issued and an empty list returned.
@ -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 });
}
/**

View File

@ -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];
}
/**

View File

@ -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) {
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this);
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);
}
}

View File

@ -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;
}
/**

View File

@ -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();
// });
});

View File

@ -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 capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
return this.translate.instant(capitalizedKey) + ': ' + this.processDetailResponse(e.error[key]);
})
.join(', ');
const errorList = Object.keys(e.error).map(key => {
const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
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.

View File

@ -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;
}
/**

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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.
}
/**

View File

@ -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
*/

View File

@ -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);
}
}
}

View File

@ -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)) };
}
/**

View File

@ -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];

View File

@ -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();
});
});

View File

@ -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);
}
/**

View File

@ -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 */

View File

@ -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>;
}

View File

@ -0,0 +1,5 @@
import { Observable } from 'rxjs';
export interface HasViewModelListObservable<V> {
getViewModelListObservable(): Observable<V[]>;
}

View File

@ -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];

View File

@ -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
*

View File

@ -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;
};

View 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;
});
}
}

View File

@ -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;
};
/**

View File

@ -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
*

View File

@ -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();
});
});

View File

@ -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');
};
}

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -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();
});
});

View File

@ -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
*

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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

View File

@ -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();
});
});

View File

@ -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.

View File

@ -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();
});
});

View File

@ -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();
});
});

View File

@ -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');
};
}

View File

@ -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();
});
});

View File

@ -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));
}
}

View File

@ -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,79 +723,221 @@ 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 affected: LineNumberRange = this.lineNumbering.getLineNumberRange(paragraph);
return {
paragraphNo: index,
html: this.lineNumbering.stripLineNumbers(paragraph),
lineFrom: affected.from,
lineTo: affected.to
};
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);
});
}
/**
* Returns all paragraphs that are affected by the given amendment in diff-format
* To create paragraph based amendments for amendments, creates diffed paragraphs
* for selection
*/
public getDiffedParagraphToChoose(amendment: ViewMotion, lineLength: number): ParagraphToChoose[] {
if (amendment.hasParent) {
const parent = amendment.parent;
return this.getTextParagraphs(parent, true, lineLength).map((paragraph: string, index: number) => {
const diffedParagraph = amendment.amendment_paragraphs[index]
? this.diff.diff(paragraph, amendment.amendment_paragraphs[index], lineLength)
: paragraph;
return this.extractAffectedParagraphs(diffedParagraph, index);
});
} else {
throw new Error('getDiffedParagraphToChoose: given amendment has no parent');
}
}
/**
* Creates a selectable and editable paragraph
*/
private extractAffectedParagraphs(paragraph: string, index: number): ParagraphToChoose {
const affected: LineNumberRange = this.lineNumbering.getLineNumberRange(paragraph);
return {
paragraphNo: index,
html: this.lineNumbering.stripLineNumbers(paragraph),
lineFrom: affected.from,
lineTo: affected.to
} as ParagraphToChoose;
}
/**
* Returns the amended paragraphs by an amendment. Correlates to the amendment_paragraphs field,
* but also considers relevant change recommendations.
* The returned array includes "null" values for paragraphs that have not been changed.
*
* @param {ViewMotion} amendment
* @param {number} lineLength
* @param {ViewMotionChangeRecommendation[]} changes
* @param {boolean} includeUnchanged
* @returns {string[]}
*/
public applyChangesToAmendment(
amendment: ViewMotion,
lineLength: number,
changes: ViewMotionChangeRecommendation[],
includeUnchanged: boolean
): string[] {
const motion = amendment.parent;
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
// Changes need to be applied from the bottom up, to prevent conflicts with changing line numbers.
changes.sort((change1: ViewUnifiedChange, change2: ViewUnifiedChange) => {
if (change1.getLineFrom() < change2.getLineFrom()) {
return 1;
} else if (change1.getLineFrom() > change2.getLineFrom()) {
return -1;
} else {
return 0;
}
});
return amendment.amendment_paragraphs?.map((newText: string, paraNo: number) => {
let paragraph: string;
let paragraphHasChanges;
if (newText === null) {
paragraph = baseParagraphs[paraNo];
paragraphHasChanges = false;
} else {
// Add line numbers to newText, relative to the baseParagraph, by creating a diff
// to the line numbered base version any applying it right away
const diff = this.diff.diff(baseParagraphs[paraNo], newText);
paragraph = this.diff.diffHtmlToFinalText(diff);
paragraphHasChanges = true;
}
const affected: LineNumberRange = this.lineNumbering.getLineNumberRange(paragraph);
changes.forEach((change: ViewMotionChangeRecommendation) => {
// Hint: this assumes that change recommendations only affect one specific paragraph, not multiple
if (change.line_from >= affected.from && change.line_from < affected.to) {
paragraph = this.diff.replaceLines(paragraph, change.text, change.line_from, change.line_to);
// Reapply relative line numbers
const diff = this.diff.diff(baseParagraphs[paraNo], paragraph);
paragraph = this.diff.diffHtmlToFinalText(diff);
paragraphHasChanges = true;
}
});
if (paragraphHasChanges || includeUnchanged) {
return paragraph;
} else {
return null;
}
});
}
/**
* Returns all paragraph lines that are affected by the given amendment in diff-format, including context.
*
* Should only be called for paragraph-based amendments.
*
* @param {ViewMotion} amendment
* @param {number} lineLength
* @param {ChangeRecoMode} crMode
* @param {ViewMotionChangeRecommendation[]} changeRecommendations
* @param {boolean} includeUnchanged
* @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) {
const paragraph_line_range = this.lineNumbering.getLineNumberRange(baseParagraphs[paraNo]);
return {
paragraphNo: paraNo,
paragraphLineFrom: paragraph_line_range.from,
paragraphLineTo: paragraph_line_range.to,
diffLineFrom: paragraph_line_range.to,
diffLineTo: paragraph_line_range.to,
textPre: baseParagraphs[paraNo],
text: '',
textPost: ''
} as DiffLinesInParagraph;
} else {
return null; // null will make this paragraph filtered out
}
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,
paragraphLineFrom: paragraph_line_range.from,
paragraphLineTo: paragraph_line_range.to,
diffLineFrom: paragraph_line_range.to,
diffLineTo: paragraph_line_range.to,
textPre: baseParagraphs[paraNo],
text: '',
textPost: ''
} as DiffLinesInParagraph;
} else {
return diffLines;
}
})
.filter((para: DiffLinesInParagraph) => para !== null);
}
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
*/
public async createPoll(motion: ViewMotion): Promise<void> {
const url = '/rest/motions/motion/' + motion.id + '/create_poll/';
await this.httpService.post(url);
}
/**
* Sends an update request for a poll.
* 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 poll
* @param {ViewMotion} amendment
* @param {number} lineLength
* @param {boolean} withDiff
* @returns {LineNumberedString[]}
*/
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
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;
}
};
await this.httpService.put(url, data);
}
/**
* 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);
const diff = this.diff.diff(origText, newText);
if (withDiff) {
return diff;
} else {
return this.diff.diffHtmlToFinalText(diff);
}
});
}
/**

View File

@ -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();
});
});

View File

@ -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');
};
}

View File

@ -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;
}
})
);
}
}

View File

@ -12,7 +12,7 @@ describe('TagRepositoryService', () => {
});
it('should be created', () => {
const service = TestBed.get(TagRepositoryService);
const service = TestBed.inject(TagRepositoryService);
expect(service).toBeTruthy();
});
});

View File

@ -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();
});
});

View File

@ -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
})
);
}
}

View File

@ -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)));
}
}

View File

@ -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:

View File

@ -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');

View File

@ -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: [

View File

@ -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;
}

View File

@ -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');
}
/**

View 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();
});
});

View 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);
}
}
}

View File

@ -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) {

View File

@ -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)) {

View 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);
}
}
}
}

View File

@ -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();
// });
});

View File

@ -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();
});
});

View File

@ -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

View File

@ -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', () => {

View File

@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
html += node.childNodes[i].nodeValue.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
} 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) => {
html = this.lineNumberingService.insertLineNumbers(html, lineLength, null, null, 1);
html = this.replaceLines(html, change.getChangeNewText(), change.getLineFrom(), change.getLineTo());
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 +

View File

@ -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]
})
);
}),
(service = TestBed.inject(DurationService));
});
it('should be created', inject([DurationService], (service: 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('');
});
});

View File

@ -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 '';
}

View File

@ -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">&nbsp;</span>Lorem ipsum dolorsit amet</p>';
* const lineNumberedHtml =
* '<p><span class="os-line-number line-number-1" data-line-number="1" contenteditable="false">&nbsp;</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,22 +377,27 @@ export class LinenumberingService {
* @returns {LineNumberRange}
*/
public getLineNumberRange(html: string): LineNumberRange {
const fragment = this.htmlToFragment(html);
const range = {
from: null,
to: null
};
const lineNumbers = fragment.querySelectorAll('.os-line-number');
for (let i = 0; i < lineNumbers.length; i++) {
const node = lineNumbers.item(i);
const number = parseInt(node.getAttribute('data-line-number'), 10);
if (range.from === null || number < range.from) {
range.from = number;
}
if (range.to === null || number + 1 > range.to) {
range.to = number + 1;
const cacheKey = this.djb2hash(html);
let range = this.lineNumberCache.get(cacheKey);
if (!range) {
const fragment = this.htmlToFragment(html);
range = {
from: null,
to: null
};
const lineNumbers = fragment.querySelectorAll('.os-line-number');
for (let i = 0; i < lineNumbers.length; i++) {
const node = lineNumbers.item(i);
const number = parseInt(node.getAttribute('data-line-number'), 10);
if (range.from === null || number < range.from) {
range.from = number;
}
if (range.to === null || number + 1 > range.to) {
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 fragment = this.htmlToFragment(html);
return this.splitNodeToParagraphs(fragment).map((node: Element): string => {
return node.outerHTML;
});
const cacheKey = this.djb2hash(html);
let cachedParagraphs = this.lineNumberCache.get(cacheKey);
if (!cachedParagraphs) {
const fragment = this.htmlToFragment(html);
cachedParagraphs = this.splitNodeToParagraphs(fragment).map((node: Element): string => {
return node.outerHTML;
});
this.lineNumberCache.put(cacheKey, cachedParagraphs);
}
return cachedParagraphs;
}
/**
@ -890,7 +911,7 @@ export class LinenumberingService {
highlight?: number,
callback?: () => void,
firstLine?: number
): string {
): LineNumberedString {
let newHtml, newRoot;
if (highlight > 0) {

View File

@ -18,7 +18,7 @@ interface ImageConfigObject {
/**
* The structure of a font config
*/
interface FontConfigObject {
export interface FontConfigObject {
display_name: string;
default: string;
path: string;

View File

@ -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();
});
});

View File

@ -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';

View File

@ -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();
}));
});

View File

@ -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 '';
}
}
}

View File

@ -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();
});
});

View File

@ -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.
}

View File

@ -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();
// });
});

View File

@ -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.
*/

View File

@ -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, {});
}
}

View File

@ -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();
});
});

View 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