From 1186bc31095d14ed67bc34ae3c08730bcdc0c6b6 Mon Sep 17 00:00:00 2001
From: Viktoriia Krasnovyd <114735598+vkrasnovyd@users.noreply.github.com>
Date: Thu, 26 Feb 2026 10:37:43 +0100
Subject: [PATCH 01/23] Feature/new_vote_service (#3167)
---
data/example-data.json | 583 ++---
docs/Actions-Overview.md | 18 -
docs/actions/meeting.update.md | 2 +-
docs/actions/meeting_user.update.md | 5 +
docs/actions/option.update.md | 23 -
docs/actions/poll.anonymize.md | 15 -
docs/actions/poll.create.md | 79 -
docs/actions/poll.delete.md | 13 -
docs/actions/poll.publish.md | 13 -
docs/actions/poll.reset.md | 15 -
docs/actions/poll.start.md | 15 -
docs/actions/poll.stop.md | 19 -
docs/actions/poll.update.md | 54 -
docs/actions/poll_candidate.create.md | 14 -
docs/actions/poll_candidate.delete.md | 13 -
docs/actions/poll_candidate_list.create.md | 17 -
docs/actions/poll_candidate_list.delete.md | 12 -
docs/actions/user.merge_together.md | 11 +-
docs/actions/user.update.md | 5 -
openslides_backend/action/actions/__init__.py | 5 -
.../action/actions/meeting/delete.py | 12 +
.../action/actions/meeting/update.py | 2 +-
.../action/actions/meeting_user/update.py | 4 +
.../action/actions/option/__init__.py | 1 -
.../action/actions/option/create.py | 96 -
.../action/actions/option/delete.py | 38 -
.../action/actions/option/set_auto_fields.py | 28 -
.../action/actions/option/update.py | 185 --
.../action/actions/poll/__init__.py | 11 -
.../action/actions/poll/anonymize.py | 76 -
.../action/actions/poll/base.py | 26 -
.../action/actions/poll/create.py | 252 --
.../action/actions/poll/delete.py | 103 -
.../action/actions/poll/functions.py | 25 -
.../action/actions/poll/mixins.py | 259 --
.../action/actions/poll/publish.py | 53 -
.../action/actions/poll/reset.py | 138 --
.../action/actions/poll/set_state.py | 17 -
.../action/actions/poll/start.py | 74 -
.../action/actions/poll/stop.py | 133 --
.../action/actions/poll/update.py | 252 --
.../action/actions/poll_candidate/__init__.py | 1 -
.../action/actions/poll_candidate/create.py | 23 -
.../action/actions/poll_candidate/delete.py | 15 -
.../actions/poll_candidate_list/__init__.py | 1 -
.../actions/poll_candidate_list/create.py | 53 -
.../actions/poll_candidate_list/delete.py | 15 -
.../action/actions/user/merge_mixins.py | 63 +-
.../action/actions/user/merge_together.py | 241 +-
.../action/actions/user/update.py | 5 -
.../action/actions/vote/__init__.py | 1 -
.../action/actions/vote/anonymize.py | 22 -
.../action/actions/vote/create.py | 75 -
.../action/actions/vote/delete.py | 15 -
.../action/actions/vote/update.py | 15 -
.../action/actions/vote/user_token_helper.py | 5 -
openslides_backend/models/checker.py | 19 +-
openslides_backend/models/mixins.py | 16 +-
openslides_backend/models/models.py | 350 ++-
openslides_backend/services/vote/adapter.py | 38 +-
openslides_backend/services/vote/interface.py | 19 +-
openslides_backend/shared/env.py | 2 +-
openslides_backend/shared/export_helper.py | 76 +-
tests/database/reader/system/util.py | 27 +-
.../system/action/agenda_item/test_forward.py | 26 +-
tests/system/action/meeting/test_archive.py | 16 +-
tests/system/action/meeting/test_clone.py | 177 +-
tests/system/action/meeting/test_delete.py | 50 +-
tests/system/action/meeting/test_import.py | 130 +-
tests/system/action/meeting/test_update.py | 5 +-
.../system/action/meeting_user/test_update.py | 80 +
tests/system/action/option/__init__.py | 0
tests/system/action/option/test_create.py | 80 -
tests/system/action/option/test_delete.py | 44 -
tests/system/action/option/test_update.py | 278 ---
.../test_delete_history_information.py | 14 +-
tests/system/action/poll/__init__.py | 0
tests/system/action/poll/poll_test_mixin.py | 45 -
tests/system/action/poll/test_anonymize.py | 139 --
tests/system/action/poll/test_create.py | 1210 ----------
tests/system/action/poll/test_delete.py | 178 --
tests/system/action/poll/test_publish.py | 98 -
tests/system/action/poll/test_reset.py | 182 --
tests/system/action/poll/test_set_state.py | 32 -
tests/system/action/poll/test_start.py | 207 --
tests/system/action/poll/test_stop.py | 370 ---
tests/system/action/poll/test_update.py | 697 ------
tests/system/action/poll/test_vote.py | 2118 -----------------
.../system/action/poll_candidate/__init__.py | 0
.../action/poll_candidate/test_create.py | 27 -
.../action/poll_candidate/test_delete.py | 27 -
.../action/poll_candidate_list/__init__.py | 0
.../action/poll_candidate_list/test_create.py | 38 -
.../action/poll_candidate_list/test_delete.py | 45 -
tests/system/action/projector/test_toggle.py | 13 +-
tests/system/action/test_internal_actions.py | 8 +-
tests/system/action/user/test_delete.py | 37 +-
.../system/action/user/test_merge_together.py | 474 ++--
tests/system/action/user/test_update.py | 121 +-
tests/system/action/vote/__init__.py | 0
tests/system/action/vote/test_create.py | 30 -
tests/system/action/vote/test_delete.py | 27 -
tests/system/action/vote/test_update.py | 26 -
tests/system/presenter/test_check_database.py | 40 +-
.../presenter/test_check_database_all.py | 40 +-
tests/system/presenter/test_export_meeting.py | 80 +-
tests/system/util.py | 14 +-
107 files changed, 1421 insertions(+), 9615 deletions(-)
delete mode 100644 docs/actions/option.update.md
delete mode 100644 docs/actions/poll.anonymize.md
delete mode 100644 docs/actions/poll.create.md
delete mode 100644 docs/actions/poll.delete.md
delete mode 100644 docs/actions/poll.publish.md
delete mode 100644 docs/actions/poll.reset.md
delete mode 100644 docs/actions/poll.start.md
delete mode 100644 docs/actions/poll.stop.md
delete mode 100644 docs/actions/poll.update.md
delete mode 100644 docs/actions/poll_candidate.create.md
delete mode 100644 docs/actions/poll_candidate.delete.md
delete mode 100644 docs/actions/poll_candidate_list.create.md
delete mode 100644 docs/actions/poll_candidate_list.delete.md
delete mode 100644 openslides_backend/action/actions/option/__init__.py
delete mode 100644 openslides_backend/action/actions/option/create.py
delete mode 100644 openslides_backend/action/actions/option/delete.py
delete mode 100644 openslides_backend/action/actions/option/set_auto_fields.py
delete mode 100644 openslides_backend/action/actions/option/update.py
delete mode 100644 openslides_backend/action/actions/poll/__init__.py
delete mode 100644 openslides_backend/action/actions/poll/anonymize.py
delete mode 100644 openslides_backend/action/actions/poll/base.py
delete mode 100644 openslides_backend/action/actions/poll/create.py
delete mode 100644 openslides_backend/action/actions/poll/delete.py
delete mode 100644 openslides_backend/action/actions/poll/functions.py
delete mode 100644 openslides_backend/action/actions/poll/mixins.py
delete mode 100644 openslides_backend/action/actions/poll/publish.py
delete mode 100644 openslides_backend/action/actions/poll/reset.py
delete mode 100644 openslides_backend/action/actions/poll/set_state.py
delete mode 100644 openslides_backend/action/actions/poll/start.py
delete mode 100644 openslides_backend/action/actions/poll/stop.py
delete mode 100644 openslides_backend/action/actions/poll/update.py
delete mode 100644 openslides_backend/action/actions/poll_candidate/__init__.py
delete mode 100644 openslides_backend/action/actions/poll_candidate/create.py
delete mode 100644 openslides_backend/action/actions/poll_candidate/delete.py
delete mode 100644 openslides_backend/action/actions/poll_candidate_list/__init__.py
delete mode 100644 openslides_backend/action/actions/poll_candidate_list/create.py
delete mode 100644 openslides_backend/action/actions/poll_candidate_list/delete.py
delete mode 100644 openslides_backend/action/actions/vote/__init__.py
delete mode 100644 openslides_backend/action/actions/vote/anonymize.py
delete mode 100644 openslides_backend/action/actions/vote/create.py
delete mode 100644 openslides_backend/action/actions/vote/delete.py
delete mode 100644 openslides_backend/action/actions/vote/update.py
delete mode 100644 openslides_backend/action/actions/vote/user_token_helper.py
delete mode 100644 tests/system/action/option/__init__.py
delete mode 100644 tests/system/action/option/test_create.py
delete mode 100644 tests/system/action/option/test_delete.py
delete mode 100644 tests/system/action/option/test_update.py
delete mode 100644 tests/system/action/poll/__init__.py
delete mode 100644 tests/system/action/poll/poll_test_mixin.py
delete mode 100644 tests/system/action/poll/test_anonymize.py
delete mode 100644 tests/system/action/poll/test_create.py
delete mode 100644 tests/system/action/poll/test_delete.py
delete mode 100644 tests/system/action/poll/test_publish.py
delete mode 100644 tests/system/action/poll/test_reset.py
delete mode 100644 tests/system/action/poll/test_set_state.py
delete mode 100644 tests/system/action/poll/test_start.py
delete mode 100644 tests/system/action/poll/test_stop.py
delete mode 100644 tests/system/action/poll/test_update.py
delete mode 100644 tests/system/action/poll_candidate/__init__.py
delete mode 100644 tests/system/action/poll_candidate/test_create.py
delete mode 100644 tests/system/action/poll_candidate/test_delete.py
delete mode 100644 tests/system/action/poll_candidate_list/__init__.py
delete mode 100644 tests/system/action/poll_candidate_list/test_create.py
delete mode 100644 tests/system/action/poll_candidate_list/test_delete.py
delete mode 100644 tests/system/action/vote/__init__.py
delete mode 100644 tests/system/action/vote/test_create.py
delete mode 100644 tests/system/action/vote/test_delete.py
delete mode 100644 tests/system/action/vote/test_update.py
diff --git a/data/example-data.json b/data/example-data.json
index 319c3c1ba3..e2ea9a0822 100644
--- a/data/example-data.json
+++ b/data/example-data.json
@@ -22,8 +22,7 @@
"4":{
"id": 4,
"name": "non-binary",
- "organization_id": 1,
- "user_ids":[]
+ "organization_id": 1
}
},
"organization": {
@@ -49,7 +48,6 @@
1
],
"theme_id": 1,
- "mediafile_ids": [],
"theme_ids": [
1,
2,
@@ -97,10 +95,6 @@
1
],
"committee_management_ids": [1],
- "poll_voted_ids": [5],
- "option_ids": [5, 7],
- "vote_ids": [9],
- "delegated_vote_ids": [9],
"meeting_user_ids": [1],
"meeting_ids": [
1
@@ -121,7 +115,6 @@
"committee_ids": [
1
],
- "option_ids": [9, 12],
"meeting_user_ids": [2],
"meeting_ids": [
1
@@ -139,7 +132,6 @@
"can_change_own_password": true,
"gender_id": 3,
"default_vote_weight": "1.000000",
- "option_ids": [8, 11],
"meeting_user_ids": [3],
"meeting_ids": [
1
@@ -164,7 +156,11 @@
"motion_submitter_ids": [1, 2, 3, 4],
"assignment_candidate_ids": [1],
"structure_level_ids": [1],
- "group_ids": [2]
+ "group_ids": [2],
+ "poll_voted_ids": [3],
+ "poll_option_ids": [1, 8],
+ "acting_ballot_ids": [9],
+ "represented_ballot_ids": [9]
},
"2": {
"id": 2,
@@ -176,6 +172,7 @@
"vote_weight": "1.000000",
"speaker_ids": [2, 3, 7, 10, 11, 13],
"assignment_candidate_ids": [3, 5],
+ "poll_option_ids": [9],
"structure_level_ids": [2],
"group_ids": [5]
},
@@ -190,6 +187,7 @@
"speaker_ids": [4, 8, 9],
"motion_supporter_ids": [1],
"assignment_candidate_ids": [2, 4],
+ "poll_option_ids": [2, 10],
"structure_level_ids": [3],
"group_ids": [5]
}
@@ -401,7 +399,8 @@
3
],
"poll_default_live_voting_enabled": false,
- "poll_default_backend": "fast",
+ "poll_default_allow_invalid": false,
+ "poll_default_allow_vote_split": false,
"poll_couple_countdown": true,
"meeting_user_ids": [1, 2, 3],
"projector_ids": [
@@ -554,32 +553,6 @@
4,
5
],
- "option_ids": [
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- 7,
- 8,
- 9,
- 10,
- 11,
- 12,
- 13
- ],
- "vote_ids": [
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- 7,
- 8,
- 9
- ],
"assignment_ids": [
1,
2
@@ -665,7 +638,6 @@
"id": 2,
"name": "Admin",
"admin_group_for_meeting_id": 1,
- "permissions": [],
"weight": 2,
"meeting_user_ids": [
1
@@ -978,7 +950,7 @@
"list_of_speakers": {
"1": {
"id": 1,
- "content_object_id": "motion/1",
+ "content_object_id": "motion/1",
"closed": false,
"speaker_ids": [
11,
@@ -989,13 +961,13 @@
},
"2": {
"id": 2,
- "content_object_id": "motion/2",
+ "content_object_id": "motion/2",
"closed": false,
"meeting_id": 1
},
"3": {
"id": 3,
- "content_object_id": "topic/1",
+ "content_object_id": "topic/1",
"closed": false,
"speaker_ids": [
1,
@@ -1005,25 +977,25 @@
},
"4": {
"id": 4,
- "content_object_id": "topic/2",
+ "content_object_id": "topic/2",
"closed": false,
"meeting_id": 1
},
"5": {
"id": 5,
- "content_object_id": "topic/3",
+ "content_object_id": "topic/3",
"closed": false,
"meeting_id": 1
},
"6": {
"id": 6,
- "content_object_id": "topic/4",
+ "content_object_id": "topic/4",
"closed": false,
"meeting_id": 1
},
"7": {
"id": 7,
- "content_object_id": "topic/5",
+ "content_object_id": "topic/5",
"closed": false,
"speaker_ids": [
3,
@@ -1033,7 +1005,7 @@
},
"8": {
"id": 8,
- "content_object_id": "topic/6",
+ "content_object_id": "topic/6",
"closed": false,
"speaker_ids": [
5
@@ -1042,19 +1014,19 @@
},
"9": {
"id": 9,
- "content_object_id": "topic/7",
+ "content_object_id": "topic/7",
"closed": false,
"meeting_id": 1
},
"10": {
"id": 10,
- "content_object_id": "topic/8",
+ "content_object_id": "topic/8",
"closed": false,
"meeting_id": 1
},
"11": {
"id": 11,
- "content_object_id": "assignment/1",
+ "content_object_id": "assignment/1",
"closed": false,
"speaker_ids": [
6,
@@ -1065,19 +1037,19 @@
},
"12": {
"id": 12,
- "content_object_id": "motion/3",
+ "content_object_id": "motion/3",
"closed": false,
"meeting_id": 1
},
"13": {
"id": 13,
- "content_object_id": "motion/4",
+ "content_object_id": "motion/4",
"closed": false,
"meeting_id": 1
},
"14": {
"id": 14,
- "content_object_id": "assignment/2",
+ "content_object_id": "assignment/2",
"closed": false,
"speaker_ids": [
9,
@@ -1087,13 +1059,13 @@
},
"15": {
"id": 15,
- "content_object_id": "motion_block/1",
+ "content_object_id": "motion_block/1",
"closed": false,
"meeting_id": 1
},
"16": {
"id": 16,
- "content_object_id": "meeting_mediafile/1",
+ "content_object_id": "meeting_mediafile/1",
"closed": false,
"meeting_id": 1
}
@@ -1197,56 +1169,60 @@
"1": {
"id": 1,
"title": "A",
- "agenda_item_id": 3,
+ "agenda_item_id": 3,
"list_of_speakers_id": 3,
- "meeting_id": 1
+ "meeting_id": 1,
+ "poll_ids": [
+ 4
+ ]
+
},
"2": {
"id": 2,
"title": "B",
- "agenda_item_id": 4,
+ "agenda_item_id": 4,
"list_of_speakers_id": 4,
"meeting_id": 1
},
"3": {
"id": 3,
"title": "C",
- "agenda_item_id": 5,
+ "agenda_item_id": 5,
"list_of_speakers_id": 5,
"meeting_id": 1
},
"4": {
"id": 4,
"title": "D",
- "agenda_item_id": 6,
+ "agenda_item_id": 6,
"list_of_speakers_id": 6,
"meeting_id": 1
},
"5": {
"id": 5,
"title": "E",
- "agenda_item_id": 7,
+ "agenda_item_id": 7,
"list_of_speakers_id": 7,
"meeting_id": 1
},
"6": {
"id": 6,
"title": "F",
- "agenda_item_id": 8,
+ "agenda_item_id": 8,
"list_of_speakers_id": 8,
"meeting_id": 1
},
"7": {
"id": 7,
"title": "G",
- "agenda_item_id": 9,
+ "agenda_item_id": 9,
"list_of_speakers_id": 9,
"meeting_id": 1
},
"8": {
"id": 8,
"title": "H",
- "agenda_item_id": 10,
+ "agenda_item_id": 10,
"list_of_speakers_id": 10,
"meeting_id": 1
}
@@ -1256,7 +1232,7 @@
"id": 1,
"number": "A1",
"number_value": 1,
- "title": "test",
+ "title": "test",
"text": "
\n- Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque
\n- penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat ma
\n- ssa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, vene
\n
\nnatis vitae, justo. Null
\nam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi.Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibu \n- s in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem.Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed consequat, leo eget bibendum sodales, augue velit cursus nunc, quis gravida magna mi a libero. Fusce vulputate eleifend sapien. Vestibulum purus quam, scelerisque ut, mollis sed, nonummy id, metus. Nullam accumsan lorem in dui. Cras ultricies mi eu turpis hendrerit fringilla. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; In ac dui quis mi consectetuer lacinia. Nam pretium turpis et arcu. Duis arcu tortor, suscipit eget, imperdiet nec, imperdiet iaculis, ipsum. Sed aliquam ultrices mauris. Integer ante arcu, accumsan a, consectetuer eget, posuere ut, mauris. Praesent adipiscing. Phasellus ullamcorper ipsum rutrum nunc. Nunc nonummy metus. Vestibulum volutpat pretium libero. Cras id dui. Aenean ut
\n
",
"modified_final_version": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi.Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem.Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed consequat, leo eget bibendum sodales, augue velit cursus nunc, quis gravida magna mi a libero. Fusce vulputate eleifend sapien. Vestibulum purus quam, scelerisque ut, mollis sed, nonummy id, metus. Nullam accumsan lorem in dui. Cras ultricies mi eu turpis hendrerit fringilla. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; In ac dui quis mi consectetuer lacinia. Nam pretium turpis et arcu. Duis arcu tortor, suscipit eget, imperdiet nec, imperdiet iaculis, ipsum. Sed aliquam ultrices mauris. Integer ante arcu, accumsan a, consectetuer eget, posuere ut, mauris. Praesent adipiscing. Phasellus ullamcorper ipsum rutrum nunc. Nunc nonummy metus. Vestibulum volutpat pretium libero. Cras id dui. Aenean ut
",
"text_hash": "2d59f168675841ecc69e3040ae3eca7c",
@@ -1274,10 +1250,6 @@
1
],
"poll_ids": [
- 1,
- 2
- ],
- "option_ids": [
1,
3
],
@@ -1292,7 +1264,7 @@
"id": 2,
"number": "1 - 1",
"number_value": 1,
- "title": "\u00c4nderungsantrag zu 1",
+ "title": "\u00c4nderungsantrag zu 1",
"text": "lömk
",
"text_hash": "0339e76557663e3a6f03b9e347409f97",
"category_weight": 10000,
@@ -1322,7 +1294,7 @@
"id": 3,
"number": "2",
"number_value": 2,
- "title": "ohne",
+ "title": "ohne",
"text": "sf
",
"text_hash": "60d31eb37595dd44584be5ef363283e3",
"category_weight": 100,
@@ -1353,7 +1325,7 @@
"id": 4,
"number": "3",
"number_value": 3,
- "title": "komplex",
+ "title": "komplex",
"text": "sdf sdfpdfkw wef
\n\nwepkf
\n\nweüpfk
\n\nweüpfdfg
",
"text_hash": "af905e34b34d2b08b60c6af41153f078",
"category_weight": 10000,
@@ -1421,7 +1393,7 @@
"id": 1,
"name": "Neu",
"weight": 10000,
- "comment_ids": [
+ "comment_ids": [
1
],
"read_group_ids": [
@@ -1442,7 +1414,7 @@
"prefix": "C",
"weight": 2,
"level": 0,
- "child_ids": [
+ "child_ids": [
2
],
"motion_ids": [
@@ -1456,7 +1428,7 @@
"prefix": "B",
"weight": 4,
"level": 1,
- "parent_id": 1,
+ "parent_id": 1,
"motion_ids": [
1,
3
@@ -1468,7 +1440,7 @@
"1": {
"id": 1,
"title": "BLOCK A",
- "motion_ids": [
+ "motion_ids": [
3,
4
],
@@ -1529,12 +1501,12 @@
],
"workflow_id": 1,
"first_state_of_workflow_id": 1,
- "restrictions": [],
"show_state_extension_field": false,
"show_recommendation_extension_field": false,
"set_workflow_timestamp": true,
"allow_motion_forwarding": true,
"allow_amendment_forwarding": false,
+ "restrictions": [],
"meeting_id": 1
},
"2": {
@@ -1549,7 +1521,6 @@
1
],
"workflow_id": 1,
- "restrictions": [],
"show_state_extension_field": false,
"show_recommendation_extension_field": false,
"allow_submitter_edit": false,
@@ -1558,6 +1529,7 @@
"set_workflow_timestamp": false,
"allow_motion_forwarding": true,
"allow_amendment_forwarding": true,
+ "restrictions": [],
"meeting_id": 1
},
"3": {
@@ -1572,7 +1544,6 @@
1
],
"workflow_id": 1,
- "restrictions": [],
"show_state_extension_field": false,
"show_recommendation_extension_field": false,
"allow_submitter_edit": false,
@@ -1581,6 +1552,7 @@
"allow_motion_forwarding": true,
"allow_amendment_forwarding": true,
"set_workflow_timestamp": false,
+ "restrictions": [],
"meeting_id": 1
},
"4": {
@@ -1595,7 +1567,6 @@
1
],
"workflow_id": 1,
- "restrictions": [],
"show_state_extension_field": false,
"show_recommendation_extension_field": false,
"allow_submitter_edit": false,
@@ -1604,6 +1575,7 @@
"allow_motion_forwarding": true,
"allow_amendment_forwarding": true,
"set_workflow_timestamp": false,
+ "restrictions": [],
"meeting_id": 1
},
"5": {
@@ -1620,7 +1592,6 @@
],
"workflow_id": 2,
"first_state_of_workflow_id": 2,
- "restrictions": [],
"show_state_extension_field": false,
"show_recommendation_extension_field": false,
"allow_create_poll": false,
@@ -1628,6 +1599,7 @@
"set_workflow_timestamp": true,
"allow_motion_forwarding": true,
"allow_amendment_forwarding": true,
+ "restrictions": [],
"meeting_id": 1
},
"6": {
@@ -1646,7 +1618,6 @@
5
],
"workflow_id": 2,
- "restrictions": [],
"show_state_extension_field": false,
"show_recommendation_extension_field": false,
"allow_submitter_edit": false,
@@ -1655,6 +1626,7 @@
"allow_motion_forwarding": true,
"allow_amendment_forwarding": true,
"set_workflow_timestamp": false,
+ "restrictions": [],
"meeting_id": 1
},
"7": {
@@ -1681,7 +1653,6 @@
4
],
"workflow_id": 2,
- "restrictions": [],
"show_state_extension_field": false,
"show_recommendation_extension_field": false,
"allow_submitter_edit": false,
@@ -1690,6 +1661,7 @@
"allow_motion_forwarding": true,
"allow_amendment_forwarding": true,
"set_workflow_timestamp": false,
+ "restrictions": [],
"meeting_id": 1
},
"8": {
@@ -1704,7 +1676,6 @@
7
],
"workflow_id": 2,
- "restrictions": [],
"show_state_extension_field": false,
"show_recommendation_extension_field": false,
"allow_submitter_edit": false,
@@ -1713,6 +1684,7 @@
"allow_motion_forwarding": true,
"allow_amendment_forwarding": true,
"set_workflow_timestamp": false,
+ "restrictions": [],
"meeting_id": 1
},
"9": {
@@ -1727,7 +1699,6 @@
7
],
"workflow_id": 2,
- "restrictions": [],
"show_state_extension_field": false,
"show_recommendation_extension_field": false,
"allow_submitter_edit": false,
@@ -1736,6 +1707,7 @@
"allow_motion_forwarding": true,
"allow_amendment_forwarding": true,
"set_workflow_timestamp": false,
+ "restrictions": [],
"meeting_id": 1
},
"10": {
@@ -1751,7 +1723,6 @@
7
],
"workflow_id": 2,
- "restrictions": [],
"show_state_extension_field": false,
"show_recommendation_extension_field": false,
"allow_submitter_edit": false,
@@ -1760,6 +1731,7 @@
"allow_motion_forwarding": true,
"allow_amendment_forwarding": true,
"set_workflow_timestamp": false,
+ "restrictions": [],
"meeting_id": 1
},
"11": {
@@ -1774,7 +1746,6 @@
7
],
"workflow_id": 2,
- "restrictions": [],
"show_state_extension_field": false,
"show_recommendation_extension_field": false,
"allow_submitter_edit": false,
@@ -1783,6 +1754,7 @@
"allow_motion_forwarding": true,
"allow_amendment_forwarding": true,
"set_workflow_timestamp": false,
+ "restrictions": [],
"meeting_id": 1
},
"12": {
@@ -1797,7 +1769,6 @@
7
],
"workflow_id": 2,
- "restrictions": [],
"show_state_extension_field": false,
"show_recommendation_extension_field": false,
"allow_submitter_edit": false,
@@ -1806,6 +1777,7 @@
"allow_motion_forwarding": true,
"allow_amendment_forwarding": true,
"set_workflow_timestamp": false,
+ "restrictions": [],
"meeting_id": 1
},
"13": {
@@ -1820,7 +1792,6 @@
7
],
"workflow_id": 2,
- "restrictions": [],
"show_state_extension_field": false,
"show_recommendation_extension_field": false,
"allow_submitter_edit": false,
@@ -1829,6 +1800,7 @@
"allow_motion_forwarding": true,
"allow_amendment_forwarding": true,
"set_workflow_timestamp": false,
+ "restrictions": [],
"meeting_id": 1
},
"14": {
@@ -1842,7 +1814,6 @@
7
],
"workflow_id": 2,
- "restrictions": [],
"show_state_extension_field": false,
"show_recommendation_extension_field": false,
"allow_submitter_edit": false,
@@ -1851,6 +1822,7 @@
"allow_motion_forwarding": true,
"allow_amendment_forwarding": true,
"set_workflow_timestamp": false,
+ "restrictions": [],
"meeting_id": 1
},
"15": {
@@ -1865,7 +1837,6 @@
6
],
"workflow_id": 2,
- "restrictions": [],
"show_state_extension_field": false,
"show_recommendation_extension_field": false,
"allow_submitter_edit": false,
@@ -1874,6 +1845,7 @@
"allow_motion_forwarding": true,
"allow_amendment_forwarding": true,
"set_workflow_timestamp": false,
+ "restrictions": [],
"meeting_id": 1
}
},
@@ -1881,7 +1853,7 @@
"1": {
"id": 1,
"name": "Simple Workflow",
- "state_ids": [
+ "state_ids": [
1,
2,
3,
@@ -1895,7 +1867,7 @@
"2": {
"id": 2,
"name": "Complex Workflow",
- "state_ids": [
+ "state_ids": [
5,
6,
7,
@@ -1916,354 +1888,243 @@
"1": {
"id": 1,
"title": "1",
- "type": "analog",
- "backend": "fast",
- "pollmethod": "YNA",
+ "config_id": "poll_config_approval/1",
+ "visibility": "manually",
"state": "finished",
- "min_votes_amount": 1,
- "max_votes_amount": 1,
- "max_votes_per_option": 1,
- "onehundred_percent_base": "YNA",
- "votesvalid": "2.000000",
- "votesinvalid": "9.000000",
- "votescast": "2.000000",
- "content_object_id": "motion/1",
- "option_ids": [
- 1
+ "published": true,
+ "allow_invalid": false,
+ "allow_vote_split": false,
+ "content_object_id": "motion/1",
+ "ballot_ids": [
+ 1,
+ 2,
+ 3
],
- "global_option_id": 2,
- "global_abstain": false,
- "global_no": false,
- "global_yes": false,
- "meeting_id": 1,
- "live_voting_enabled": false
+ "meeting_id": 1
},
"2": {
"id": 2,
"title": "2",
- "type": "analog",
- "backend": "fast",
- "pollmethod": "YNA",
- "state": "created",
- "min_votes_amount": 1,
- "max_votes_amount": 1,
- "max_votes_per_option": 1,
- "onehundred_percent_base": "YNA",
- "content_object_id": "motion/1",
- "option_ids": [
- 3
- ],
- "global_option_id": 4,
- "global_abstain": false,
- "global_no": false,
- "global_yes": false,
- "meeting_id": 1,
- "live_voting_enabled": false
+ "config_id": "poll_config_approval/2",
+ "visibility": "manually",
+ "state": "finished",
+ "published": true,
+ "allow_invalid": false,
+ "allow_vote_split": false,
+ "content_object_id": "assignment/1",
+ "meeting_id": 1
},
"3": {
"id": 3,
- "title": "1",
- "type": "analog",
- "backend": "fast",
- "pollmethod": "YNA",
- "state": "created",
- "min_votes_amount": 1,
- "max_votes_amount": 1,
- "max_votes_per_option": 1,
- "global_yes": false,
- "global_no": true,
- "global_abstain": true,
- "onehundred_percent_base": "YNA",
- "content_object_id": "assignment/1",
- "option_ids": [
- 5
- ],
- "global_option_id": 6,
- "meeting_id": 1,
- "live_voting_enabled": false
+ "title": "3",
+ "config_id": "poll_config_selection/1",
+ "visibility": "open",
+ "state": "started",
+ "published": false,
+ "allow_invalid": false,
+ "allow_vote_split": false,
+ "content_object_id": "motion/1",
+ "ballot_ids": [9],
+ "voted_ids": [1],
+ "meeting_id": 1
},
"4": {
"id": 4,
- "title": "2",
- "type": "analog",
- "backend": "fast",
- "pollmethod": "Y",
- "state": "finished",
- "min_votes_amount": 1,
- "max_votes_amount": 1,
- "max_votes_per_option": 1,
- "global_yes": false,
- "global_no": true,
- "global_abstain": true,
- "onehundred_percent_base": "Y",
- "votesvalid": "9.000000",
- "votesinvalid": "2.000000",
- "votescast": "16.000000",
- "content_object_id": "assignment/1",
- "option_ids": [
- 7,
- 8,
- 9
- ],
- "global_option_id": 10,
- "meeting_id": 1,
- "live_voting_enabled": false
+ "title": "4",
+ "config_id": "poll_config_rating_score/1",
+ "visibility": "secret",
+ "state": "created",
+ "published": false,
+ "allow_invalid": false,
+ "allow_vote_split": false,
+ "content_object_id": "topic/1",
+ "meeting_id": 1
},
"5": {
"id": 5,
- "title": "Wahlgang",
- "type": "named",
- "backend": "fast",
- "pollmethod": "Y",
+ "title": "5",
+ "config_id": "poll_config_rating_approval/1",
+ "visibility": "manually",
"state": "finished",
- "min_votes_amount": 1,
- "max_votes_amount": 1,
- "max_votes_per_option": 1,
- "global_yes": false,
- "global_no": true,
- "global_abstain": false,
- "onehundred_percent_base": "valid",
- "votesvalid": "1.000000",
- "votesinvalid": "0.000000",
- "votescast": "1.000000",
- "content_object_id": "assignment/2",
- "voted_ids": [
- 1
- ],
- "entitled_group_ids": [
- 2
- ],
- "option_ids": [
- 11,
- 12
+ "published": false,
+ "allow_invalid": false,
+ "allow_vote_split": false,
+ "content_object_id": "assignment/1",
+ "ballot_ids": [
+ 4,
+ 5,
+ 6,
+ 7,
+ 8
],
- "global_option_id": 13,
- "meeting_id": 1,
- "live_voting_enabled": false
+ "entitled_group_ids": [2],
+ "meeting_id": 1
}
},
- "option": {
+ "poll_config_approval": {
"1": {
"id": 1,
- "yes": "2.000000",
- "no": "4.000000",
- "abstain": "1.000000",
- "weight": 1,
"poll_id": 1,
- "content_object_id": "motion/1",
- "vote_ids": [
- 1,
- 2,
- 3
- ],
- "meeting_id": 1
+ "allow_abstain": false
},
"2": {
"id": 2,
- "yes": "0.000000",
- "no": "0.000000",
- "abstain": "0.000000",
+ "poll_id": 2,
+ "allow_abstain": true,
+ "option_ids": [1, 2]
+ }
+ },
+ "poll_config_selection": {
+ "1": {
+ "id": 1,
+ "poll_id": 3,
+ "option_ids": [3, 4, 5],
+ "max_options_amount": 2,
+ "min_options_amount": 1,
+ "allow_nota": true
+ }
+ },
+ "poll_config_rating_score": {
+ "1": {
+ "id": 1,
+ "poll_id": 4,
+ "option_ids": [6, 7],
+ "max_options_amount": 2,
+ "min_options_amount": 2,
+ "max_votes_per_option": 0,
+ "max_vote_sum": 5,
+ "min_vote_sum": 1
+ }
+ },
+ "poll_config_rating_approval": {
+ "1": {
+ "id": 1,
+ "poll_id": 5,
+ "max_options_amount": 0,
+ "min_options_amount": 0,
+ "allow_abstain": true,
+ "option_ids": [8, 9, 10]
+ }
+ },
+ "poll_config_option": {
+ "1": {
+ "id": 1,
"weight": 1,
- "used_as_global_option_in_poll_id": 1,
- "meeting_id": 1
+ "poll_config_id": "poll_config_approval/2",
+ "meeting_user_id": 1
+ },
+ "2": {
+ "id": 2,
+ "weight": 2,
+ "poll_config_id": "poll_config_approval/2",
+ "meeting_user_id": 3
},
"3": {
"id": 3,
- "yes": "0.000000",
- "no": "0.000000",
- "abstain": "0.000000",
- "weight": 1,
- "poll_id": 2,
- "content_object_id": "motion/1",
- "meeting_id": 1
+ "poll_config_id": "poll_config_selection/1",
+ "text": "Option 3"
},
"4": {
"id": 4,
- "yes": "0.000000",
- "no": "0.000000",
- "abstain": "0.000000",
- "weight": 1,
- "used_as_global_option_in_poll_id": 2,
- "meeting_id": 1
+ "poll_config_id": "poll_config_selection/1",
+ "text": "Option 4"
},
"5": {
"id": 5,
- "yes": "0.000000",
- "no": "0.000000",
- "abstain": "0.000000",
- "weight": 1,
- "poll_id": 3,
- "content_object_id": "user/1",
- "meeting_id": 1
+ "poll_config_id": "poll_config_selection/1",
+ "text": "Option 5"
},
"6": {
"id": 6,
- "yes": "0.000000",
- "no": "0.000000",
- "abstain": "0.000000",
- "weight": 1,
- "used_as_global_option_in_poll_id": 3,
- "meeting_id": 1
+ "poll_config_id": "poll_config_rating_score/1",
+ "text": "option 6"
},
"7": {
"id": 7,
- "yes": "3.000000",
- "no": "0.000000",
- "abstain": "0.000000",
- "weight": 1,
- "poll_id": 4,
- "content_object_id": "user/1",
- "vote_ids": [
- 4
- ],
- "meeting_id": 1
+ "poll_config_id": "poll_config_rating_score/1",
+ "text": "option 7"
},
"8": {
"id": 8,
- "yes": "7.000000",
- "no": "0.000000",
- "abstain": "0.000000",
- "weight": 2,
- "poll_id": 4,
- "content_object_id": "user/3",
- "vote_ids": [
- 5
- ],
- "meeting_id": 1
+ "poll_config_id": "poll_config_rating_approval/1",
+ "meeting_user_id": 1
},
"9": {
"id": 9,
- "yes": "2.000000",
- "no": "0.000000",
- "abstain": "0.000000",
- "weight": 3,
- "poll_id": 4,
- "content_object_id": "user/2",
- "vote_ids": [
- 6
- ],
- "meeting_id": 1
+ "poll_config_id": "poll_config_rating_approval/1",
+ "meeting_user_id": 2
},
"10": {
"id": 10,
- "yes": "0.000000",
- "no": "2.000000",
- "abstain": "1.000000",
- "weight": 1,
- "used_as_global_option_in_poll_id": 4,
- "vote_ids": [
- 7,
- 8
- ],
- "meeting_id": 1
- },
- "11": {
- "id": 11,
- "yes": "0.000000",
- "no": "0.000000",
- "abstain": "0.000000",
- "weight": 1,
- "poll_id": 5,
- "content_object_id": "user/3",
- "meeting_id": 1
- },
- "12": {
- "id": 12,
- "yes": "1.000000",
- "no": "0.000000",
- "abstain": "0.000000",
- "weight": 2,
- "poll_id": 5,
- "content_object_id": "user/2",
- "vote_ids": [
- 9
- ],
- "meeting_id": 1
- },
- "13": {
- "id": 13,
- "yes": "0.000000",
- "no": "0.000000",
- "abstain": "0.000000",
- "weight": 1,
- "used_as_global_option_in_poll_id": 5,
- "meeting_id": 1
+ "poll_config_id": "poll_config_rating_approval/1",
+ "meeting_user_id": 3
}
},
- "vote": {
+ "ballot": {
"1": {
"id": 1,
"weight": "2.000000",
- "value": "Y",
- "user_token": "SNuxJc7W93bnhAiA",
- "option_id": 1,
- "meeting_id": 1
+ "value": "yes",
+ "split": false,
+ "poll_id": 1
},
"2": {
"id": 2,
"weight": "4.000000",
- "value": "N",
- "user_token": "4bgn4RBjNlIeO7vj",
- "option_id": 1,
- "meeting_id": 1
+ "value": "no",
+ "split": false,
+ "poll_id": 1
},
"3": {
"id": 3,
"weight": "1.000000",
- "value": "A",
- "user_token": "xLBFgo3O1pAfGZ0h",
- "option_id": 1,
- "meeting_id": 1
+ "value": "abstain",
+ "split": false,
+ "poll_id": 1
},
"4": {
"id": 4,
- "value": "Y",
+ "value": "{\"1\":\"yes\",\"2\":\"abstain\",\"3\":\"yes\"}",
"weight": "3.000000",
- "user_token": "neT9r5YkT9U8yJfa",
- "option_id": 7,
- "meeting_id": 1
+ "split": false,
+ "poll_id": 5
},
"5": {
"id": 5,
- "value": "Y",
+ "value": "{\"1\":\"no\",\"2\":\"yes\",\"3\":\"yes\"}",
"weight": "7.000000",
- "user_token": "U5YSuLUI1G5rNOHn",
- "option_id": 8,
- "meeting_id": 1
+ "split": false,
+ "poll_id": 5
},
"6": {
"id": 6,
- "value": "Y",
+ "value": "{\"1\":\"yes\",\"2\":\"no\",\"3\":\"no\"}",
"weight": "2.000000",
- "user_token": "jkNKIiJr8Dl0yOXI",
- "option_id": 9,
- "meeting_id": 1
+ "split": false,
+ "poll_id": 5
},
"7": {
"id": 7,
- "value": "N",
+ "value": "{\"1\":\"no\",\"2\":\"no\",\"3\":\"yes\"}",
"weight": "2.000000",
- "user_token": "Z1cxOviuelzPT2rm",
- "option_id": 10,
- "meeting_id": 1
+ "split": false,
+ "poll_id": 5
},
"8": {
"id": 8,
- "value": "A",
+ "value": "{\"1\":\"abstain\",\"2\":\"yes\",\"3\":\"abstain\"}",
"weight": "1.000000",
- "user_token": "daUZh16fXCAu5DBL",
- "option_id": 10,
- "meeting_id": 1
+ "split": false,
+ "poll_id": 5
},
"9": {
"id": 9,
- "value": "Y",
+ "value": "1",
"weight": "1.000000",
- "user_token": "ivgipZ18D9Xac8pd",
- "user_id": 1,
- "delegated_user_id": 1,
- "option_id": 12,
- "meeting_id": 1
+ "acting_meeting_user_id": 1,
+ "represented_meeting_user_id": 1,
+ "split": false,
+ "poll_id": 3
}
},
"assignment": {
@@ -2272,14 +2133,14 @@
"title": "Wahl",
"open_posts": 1,
"phase": "voting",
- "candidate_ids": [
+ "candidate_ids": [
1,
2,
3
],
"poll_ids": [
- 3,
- 4
+ 2,
+ 5
],
"agenda_item_id": 11,
"list_of_speakers_id": 11,
@@ -2295,13 +2156,11 @@
"open_posts": 1,
"phase": "search",
"number_poll_candidates": true,
- "candidate_ids": [
+ "candidate_ids": [
4,
5
],
- "poll_ids": [
- 5
- ],
+ "poll_ids": [],
"agenda_item_id": 14,
"list_of_speakers_id": 14,
"tag_ids": [
@@ -2398,7 +2257,7 @@
"show_title": true,
"show_logo": true,
"show_clock": true,
- "current_projection_ids": [
+ "current_projection_ids": [
3,
4
],
@@ -2443,7 +2302,7 @@
"show_title": true,
"show_logo": true,
"show_clock": true,
- "used_as_default_projector_for_list_of_speakers_in_meeting_id": 1,
+ "used_as_default_projector_for_list_of_speakers_in_meeting_id": 1,
"used_as_default_projector_for_current_los_in_meeting_id": 1,
"meeting_id": 1
}
diff --git a/docs/Actions-Overview.md b/docs/Actions-Overview.md
index 38b87b1064..0a30df6fc6 100644
--- a/docs/Actions-Overview.md
+++ b/docs/Actions-Overview.md
@@ -243,21 +243,3 @@ A more general format description see in [Action-Service](https://github.com/Ope
- [account.import](actions/account.import.md)
- [participant.json_upload](actions/participant.json_upload.md)
- [participant.import](actions/participant.import.md)
-
-## Voting
-
-- [option.update](actions/option.update.md)
-- [poll.create](actions/poll.create.md)
-- [poll.delete](actions/poll.delete.md)
-- [poll.update](actions/poll.update.md)
-- [poll.start](actions/poll.start.md)
-- [poll.stop](actions/poll.stop.md)
-- [poll.publish](actions/poll.publish.md)
-- [poll.reset](actions/poll.reset.md)
-- [poll.anonymize](actions/poll.anonymize.md)
-- [poll.vote](actions/poll.vote.md)
-- [poll_candidate_list.create](actions/poll_candidate_list.create.md)
-- [poll_candidate_list.delete](actions/poll_candidate_list.delete.md)
-- [poll_candidate.create](actions/poll_candidate.create.md)
-- [poll_candidate.delete](actions/poll_candidate.delete.md)
-
diff --git a/docs/actions/meeting.update.md b/docs/actions/meeting.update.md
index 1c3dd1ea0a..f1831f866a 100644
--- a/docs/actions/meeting.update.md
+++ b/docs/actions/meeting.update.md
@@ -161,8 +161,8 @@
topic_poll_default_group_ids: Id[];
- poll_default_backend: string;
poll_default_live_voting_enabled: boolean;
+ poll_default_allow_invalid: boolean;
// Group B
present_user_ids: user/is_present_in_meeting_ids;
diff --git a/docs/actions/meeting_user.update.md b/docs/actions/meeting_user.update.md
index a96c32e0b6..aeb77b500d 100644
--- a/docs/actions/meeting_user.update.md
+++ b/docs/actions/meeting_user.update.md
@@ -5,6 +5,11 @@
id: Id;
// Optional
+ poll_option_ids: Id[];
+ poll_voted_ids: Id[];
+ acting_ballot_ids: Id[];
+ represented_ballot_ids: Id[];
+
// Group A
number: string;
structure_level_ids: Id[];
diff --git a/docs/actions/option.update.md b/docs/actions/option.update.md
deleted file mode 100644
index d34d66b473..0000000000
--- a/docs/actions/option.update.md
+++ /dev/null
@@ -1,23 +0,0 @@
-## Payload
-```js
-{
-// Required
- id: Id;
-// Optional.
- Y: number;
- N: number;
- A: number;
- publish_immediately: boolean;
-}
-```
-
-## Action
-This action is only allowed for analog polls. Updating this option changes the associated `vote` objects for `Y`, `N` and `A` to the given values.
-
-If the poll's state is *created* and at least one vote value is given (`Y`, `N` or `A`), the state must be set to *finished*. if additionally `publish_immediately` is given, the state must be set to *published*.
-
-## Permissions
-The request user needs:
-- `motion.can_manage_polls` if the poll's content object is a motion
-- `assignment.can_manage` if the poll's content object is an assignment
-- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/poll.anonymize.md b/docs/actions/poll.anonymize.md
deleted file mode 100644
index 33a32283b0..0000000000
--- a/docs/actions/poll.anonymize.md
+++ /dev/null
@@ -1,15 +0,0 @@
-## Payload
-```js
-{id: Id;}
-```
-
-## Action
-Only for non-analog polls in the state *finished* or *published*. Sets all `vote/user_id` and `vote/delegated_user_id` references to `None` for each vote of each option of the poll (including the global option).
-
-`is_pseudoanonymized` has to be set to `true`.
-
-## Permissions
-The request user needs:
-- `motion.can_manage_polls` if the poll's content object is a motion
-- `assignment.can_manage_polls` if the poll's content object is an assignment
-- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/poll.create.md b/docs/actions/poll.create.md
deleted file mode 100644
index 6738dc367a..0000000000
--- a/docs/actions/poll.create.md
+++ /dev/null
@@ -1,79 +0,0 @@
-## Payload
-
-Helper Interface for options to create:
-```js
-Interface Option {
- // Exactly one of text, content_object_id or poll_candidate_user_ids must be given
- text: string, // topic-poll
- content_object_id: Fqid, // must be one of user or motion.
- poll_candidate_user_ids: [user_ids], // sorted list of user ids for candidate list election
-
- // Optionally and only for type==analog, votes can be given
- Y: decimal(6), // Y, YN, YNA mode
- N: decimal(6), // N, YN, YNA mode
- A: decimal(6) // YNA mode
-}
-```
-
-Payload:
-```js
-{
-// Required
- title: string,
- type: string,
- pollmethod: string,
-
- meeting_id: Id,
- options: Option[], // must have at least one entry.
-
-// Optional
- content_object_id: Fqid,
- description: string,
- min_votes_amount: number,
- max_votes_amount: number,
- max_votes_per_option: number,
- global_yes: boolean,
- global_no: boolean,
- global_abstain: boolean,
- onehundred_percent_base: string,
- backend: string,
-
-// Optional, only for type==named
- live_voting_enabled: boolean,
-
-// Only for non analog types
- entitled_group_ids: Id[],
-
-// Only for type==analog
- publish_immediately: boolean,
-
-// Optionally and only for type==analog, votes can be given
- votesvalid: decimal(6),
- votesinvalid: decimal(6),
- votescast: decimal(6),
- amount_global_yes: decimal(6),
- amount_global_no: decimal(6),
- amount_global_abstain: decimal(6)
-}
-```
-
-## Action
-If an analog poll with votes is given, the state is set to `finished` if at least one vote value is given. if `publish_immediately` is true and some vote value is given, the state is set to `published`. All options given are created as instances of the `option` model. If some options have values (for analog polls), `vote` objects have to be created, one for each option and vote value (`Y`, `N`, `A`).
-
-The options must be unique in the way that each non-empty `text` and non-empty `content_object_id` can only exist once. The `option/weight` is set in the order the options are given in the payload. A global option is created.
-
-If the `type` is `pseudoanonymous`, `is_pseudoanonymized` is set to `true`.
-
-If the `content_object_id` points to a `motion` and the `motion_state` of the motion misses `allow_create_poll`, it is forbidden to create a poll.
-
-The `entitled_group_ids` may not contain the meetings `anonymous_group_id`.
-
-The `max_votes_per_option` and `min_votes_amount` must be smaller or equal to `max_votes_amount`.
-
-The `live_voting_enabled` could be set for named motion and certain named assignment polls. Assignment polls also need pollmethod `Y` and not `global_yes` and `max_votes_amount` of 1 to set the option.
-
-## Permissions
-The request user needs:
-- `motion.can_manage_polls` if the poll's content object is a motion
-- `assignment.can_manage_polls` if the poll's content object is an assignment
-- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/poll.delete.md b/docs/actions/poll.delete.md
deleted file mode 100644
index b8a20ff1d1..0000000000
--- a/docs/actions/poll.delete.md
+++ /dev/null
@@ -1,13 +0,0 @@
-## Payload
-```js
-{ id: Id; }
-```
-
-## Action
-Deletes the given poll and all linked options with all votes, too.
-
-## Permissions
-The request user needs:
-- `motion.can_manage_polls` if the poll's content object is a motion
-- `assignment.can_manage_polls` if the poll's content object is an assignment
-- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/poll.publish.md b/docs/actions/poll.publish.md
deleted file mode 100644
index df746c673a..0000000000
--- a/docs/actions/poll.publish.md
+++ /dev/null
@@ -1,13 +0,0 @@
-## Payload
-```js
-{id: Id;}
-```
-
-## Action
-Sets the state to *published*. Only allowed for polls in the *finished* state.
-
-## Permissions
-The request user needs:
-- `motion.can_manage_polls` if the poll's content object is a motion
-- `assignment.can_manage_polls` if the poll's content object is an assignment
-- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/poll.reset.md b/docs/actions/poll.reset.md
deleted file mode 100644
index 50516579a8..0000000000
--- a/docs/actions/poll.reset.md
+++ /dev/null
@@ -1,15 +0,0 @@
-## Payload
-```js
-{id: Id;}
-```
-
-## Action
-Sets the state to *created*. Only allowed for polls in the *finished* or *published* state. All vote objects of all options (including the global option) are deleted.
-
-If `type != "pseudoanonymous"`, `is_pseudoanonymized` may be reset to `false` (if it was previously `true`).
-
-## Permissions
-The request user needs:
-- `motion.can_manage_polls` if the poll's content object is a motion
-- `assignment.can_manage_polls` if the poll's content object is an assignment
-- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/poll.start.md b/docs/actions/poll.start.md
deleted file mode 100644
index 5f98abde1a..0000000000
--- a/docs/actions/poll.start.md
+++ /dev/null
@@ -1,15 +0,0 @@
-## Payload
-```js
-{id: Id;}
-```
-
-## Action
-Sets the state to *started*. Only allowed for polls in the *created* state.
-
-If `meeting/poll_couple_countdown` is true and the poll is an electronic poll, the countdown given by `meeting/poll_countdown_id` must be *restarted* (see [Countdowns](https://github.com/OpenSlides/OpenSlides/wiki/Countdowns#restart-a-countdown)).
-
-## Permissions
-The request user needs:
-- `motion.can_manage_polls` if the poll's content object is a motion
-- `assignment.can_manage_polls` if the poll's content object is an assignment
-- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/poll.stop.md b/docs/actions/poll.stop.md
deleted file mode 100644
index b1986e1fee..0000000000
--- a/docs/actions/poll.stop.md
+++ /dev/null
@@ -1,19 +0,0 @@
-## Payload
-```js
-{id: Id;}
-```
-
-## Action
-Sets the state to *finished*. Only allowed for polls in the *started* state.
-
-If `meeting/poll_couple_countdown` is true, the countdown given by `meeting/poll_countdown_id` is *reset* (see [Countdowns](https://github.com/OpenSlides/OpenSlides/wiki/Countdowns#reset-a-countdown)).
-
-Some fields are calculated upon stopping a poll:
-- The fields `votescast`, `votesvalid` and `votesinvalid` are filled (see [poll results](https://github.com/OpenSlides/OpenSlides/wiki/Voting#poll-results)). They are only filled once when the poll stops to prevent any changes e.g. from deleting users.
-- `entitled_users_at_stop` is filled. It is an array of objects which represents all users entitled to vote at the stopping point of the poll. The syntax is `{"user_id": Id, "voted": boolean, "vote_delegated_to_id": Id | null}`. The fields should be self-explanatory. This field is also a snapshot like the ones above.
-
-## Permissions
-The request user needs:
-- `motion.can_manage_polls` if the poll's content object is a motion
-- `assignment.can_manage_polls` if the poll's content object is an assignment
-- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/poll.update.md b/docs/actions/poll.update.md
deleted file mode 100644
index 350efd4eb6..0000000000
--- a/docs/actions/poll.update.md
+++ /dev/null
@@ -1,54 +0,0 @@
-## Payload
-```js
-{
-// Required
- id: Id,
-
-// Optional, only if state == created
- pollmethod: string,
- min_votes_amount: number,
- max_votes_amount: number,
- max_votes_per_option: number,
- global_yes: boolean,
- global_no: boolean,
- global_abstain: boolean,
- backend: string,
-
-// Optional, only if state == created, only for non analog types
- entitled_group_ids: Id[],
-
-// Optional, every state
- title: string,
- description: string,
- onehundred_percent_base: string,
-
-// Optional, type==analog, every state
- votesvalid: number,
- votesinvalid: number,
- votescast: number,
- publish_immediately: boolean,
-
-// Optional, type==named
- live_voting_enabled: boolean
-
-// action called internally
- entitled_users_at_stop: json
-}
-```
-
-## Action
-For analog polls: If the state is created and at least one vote value is given (`votesvalid`/`votesinvalid`/`votescast`), the state is set to finished. If additionally `publish_immediately` is given, the state is set to published.
-
-For electronic polls some fields can only be updated, if the state is *created*.
-
-The `entitled_group_ids` may not contain the meetings `anonymous_group_id`.
-
-The `max_votes_per_option` and `min_votes_amount` must be smaller or equal to `max_votes_amount` after the model has been updated.
-
-The `live_voting_enabled` could be set for named motion and certain named assignment polls. Assignment polls also need pollmethod `Y` and not `global_yes` and `max_votes_amount` of 1 to set the option.
-
-## Permissions
-The request user needs:
-- `motion.can_manage_polls` if the poll's content object is a motion
-- `assignment.can_manage_polls` if the poll's content object is an assignment
-- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/poll_candidate.create.md b/docs/actions/poll_candidate.create.md
deleted file mode 100644
index fdf4c0b674..0000000000
--- a/docs/actions/poll_candidate.create.md
+++ /dev/null
@@ -1,14 +0,0 @@
-## Payload
-
-```js
-{
- // Required
- user_id: Id;
- poll_candidate_list_id: Id;
- weight: number;
- meeting_id: Id;
-}
-```
-
-## Internal action
-Creates a poll candidate.
diff --git a/docs/actions/poll_candidate.delete.md b/docs/actions/poll_candidate.delete.md
deleted file mode 100644
index 1862c0eee8..0000000000
--- a/docs/actions/poll_candidate.delete.md
+++ /dev/null
@@ -1,13 +0,0 @@
-## Payload
-
-Payload:
-```js
-{
- // Required
- id: Id;
-}
-```
-
-## Internal action
-Deletes a poll candidate with the given id.
-
\ No newline at end of file
diff --git a/docs/actions/poll_candidate_list.create.md b/docs/actions/poll_candidate_list.create.md
deleted file mode 100644
index b11a0e3597..0000000000
--- a/docs/actions/poll_candidate_list.create.md
+++ /dev/null
@@ -1,17 +0,0 @@
-## Payload
-
-Payload:
-```js
-{
- // Required
- option_id: Id,
- meeting_id: Id,
- entries: [{
- user_id: Id,
- weight: number
- }]
-}
-```
-
-## Internal action
-Creates poll candidates for the entries and creates a `poll_candidate_list`.
diff --git a/docs/actions/poll_candidate_list.delete.md b/docs/actions/poll_candidate_list.delete.md
deleted file mode 100644
index ff65d8eda2..0000000000
--- a/docs/actions/poll_candidate_list.delete.md
+++ /dev/null
@@ -1,12 +0,0 @@
-## Payload
-
-Payload:
-```js
-{
- // Required
- id: Id;
-}
-```
-
-## Internal action
-Deletes the `poll_candidate_list` with the given id and all poll candidates which belong to this list.
\ No newline at end of file
diff --git a/docs/actions/user.merge_together.md b/docs/actions/user.merge_together.md
index 2dacb90eea..0f052932e0 100644
--- a/docs/actions/user.merge_together.md
+++ b/docs/actions/user.merge_together.md
@@ -42,11 +42,10 @@ An error is thrown if:
- Any of the secondary users have a `saml_id`
- There are multiple different `member_number`s between the selected users (empty does not count)
- There are conflicts regarding polls, i.e. two or more of the selected users...
- - are option content_objects (not counting poll_candidate_list membership) on the same poll
- - are candidates on the same poll_candidate_list
- - have voted on the same poll (delegated or not)
- Any affected meeting_users have groups that are currently entitled to work on any poll
- Any affected meeting_users _who share a meeting_:
+ - are meeting_users of the poll_config_option on the same poll
+ - have voted on the same poll (delegated or not)
- have running speakers
- are in a meeting without `list_of_speakers_allow_multiple_speakers` and have waiting speakers on the same list who cannot be merged together into at most one point_of_order and one normal speech. Speeches may not be merged if there are multiple different values (empty does count) within any of the fields:
- `speech_state`
@@ -70,8 +69,6 @@ Any date in the custom payload data from the request overwrites anything that wo
Data validity of the results is checked according to user.update rules.
The secondary users are deleted.
-
-Any poll that contains the id of any secondary user in its `entitled_users_at_stop` list will have it re-written to _additionally_ contain the new user id.
This means that a line
`{"voted": false, "present": true, "user_id": 4, "vote_delegated_to_user_id": 7}`
becomes
@@ -80,7 +77,7 @@ after two merges where for the first `user/4` was merged into `user/2` and for t
This is to ensure that the client can recognize where users were merged, as simply replacing the ids may cause situations where a user is present on a list twice and not replacing them would mean that the user that voted would not be recognizable anymore.
#### Merging of sub-collections
-Relation lists where simple unification does not suffice (usually because the target collections function mostly as a type of m:n connection between two other collections) are merged.
+Relation lists where simple unification does not suffice (usually because the target collections function mostly as a type of m:n connection between two other collections) are merged.
For that purpose, target models for these relations are compared and those that are judged to fulfill equivalent roles are grouped together.
@@ -141,7 +138,7 @@ He also needs a organization management level that is equal or higher than that
The client could/should fill the optional fields from a chosen "main" user to not force the editor to rewrite all the data.
-Warnings should be shown alerting the user that
+Warnings should be shown alerting the user that
- this action is not reversable,
- will potentially change/overwrite data in archived meetings and
- will neither port the history information of the secondary users to the new ones nor rewrite the user id in the "Changed by" column
\ No newline at end of file
diff --git a/docs/actions/user.update.md b/docs/actions/user.update.md
index 4244f4c135..a2fa5e1286 100644
--- a/docs/actions/user.update.md
+++ b/docs/actions/user.update.md
@@ -58,11 +58,6 @@
// only internal
is_present_in_meeting_ids: Id[];
- option_ids: Id[];
- poll_candidate_ids: Id[];
- poll_voted_ids: Id[];
- vote_ids: Id[];
- delegated_vote_ids: Id[];
}
```
diff --git a/openslides_backend/action/actions/__init__.py b/openslides_backend/action/actions/__init__.py
index d2c34501db..61b30f9904 100644
--- a/openslides_backend/action/actions/__init__.py
+++ b/openslides_backend/action/actions/__init__.py
@@ -33,14 +33,10 @@ def prepare_actions_map() -> None:
motion_supporter,
motion_workflow,
motion_working_group_speaker,
- option,
organization,
organization_tag,
personal_note,
point_of_order_category,
- poll,
- poll_candidate,
- poll_candidate_list,
projection,
projector,
projector_countdown,
@@ -52,7 +48,6 @@ def prepare_actions_map() -> None:
theme,
topic,
user,
- vote,
)
diff --git a/openslides_backend/action/actions/meeting/delete.py b/openslides_backend/action/actions/meeting/delete.py
index 4968b73213..a30f78649a 100644
--- a/openslides_backend/action/actions/meeting/delete.py
+++ b/openslides_backend/action/actions/meeting/delete.py
@@ -25,3 +25,15 @@ def get_committee_id(self, instance: dict[str, Any]) -> int:
["committee_id"],
)
return meeting["committee_id"]
+
+ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
+ """
+ Handle deletion of polls and all the related instances in the vote service.
+ """
+ poll_ids = self.datastore.get(f"meeting/{instance['id']}", ["poll_ids"]).get(
+ "poll_ids", []
+ )
+ for poll_id in poll_ids:
+ self.vote_service.delete(poll_id)
+ self.datastore.apply_to_be_deleted(f"poll/{poll_id}")
+ return instance
diff --git a/openslides_backend/action/actions/meeting/update.py b/openslides_backend/action/actions/meeting/update.py
index 5fa0654533..6a798ac720 100644
--- a/openslides_backend/action/actions/meeting/update.py
+++ b/openslides_backend/action/actions/meeting/update.py
@@ -171,8 +171,8 @@
"assignment_poll_default_group_ids",
"assignment_poll_default_backend",
"topic_poll_default_group_ids",
- "poll_default_backend",
"poll_default_live_voting_enabled",
+ "poll_default_allow_invalid",
]
diff --git a/openslides_backend/action/actions/meeting_user/update.py b/openslides_backend/action/actions/meeting_user/update.py
index 7b59420094..f1703a83a2 100644
--- a/openslides_backend/action/actions/meeting_user/update.py
+++ b/openslides_backend/action/actions/meeting_user/update.py
@@ -40,6 +40,10 @@ class MeetingUserUpdate(
optional_properties=[
"about_me",
"group_ids",
+ "poll_option_ids",
+ "poll_voted_ids",
+ "acting_ballot_ids",
+ "represented_ballot_ids",
*meeting_user_standard_fields,
*merge_fields,
],
diff --git a/openslides_backend/action/actions/option/__init__.py b/openslides_backend/action/actions/option/__init__.py
deleted file mode 100644
index a27b3acfe9..0000000000
--- a/openslides_backend/action/actions/option/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import create, delete, set_auto_fields, update # noqa
diff --git a/openslides_backend/action/actions/option/create.py b/openslides_backend/action/actions/option/create.py
deleted file mode 100644
index 854f75768e..0000000000
--- a/openslides_backend/action/actions/option/create.py
+++ /dev/null
@@ -1,96 +0,0 @@
-from typing import Any
-
-from openslides_backend.shared.schema import id_list_schema
-
-from ....models.models import Option
-from ....shared.exceptions import ActionException
-from ...generics.create import CreateAction
-from ...util.action_type import ActionType
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-from ..poll_candidate_list.create import PollCandidateListCreate
-from ..vote.create import VoteCreate
-from ..vote.user_token_helper import get_user_token
-
-
-@register_action("option.create", action_type=ActionType.BACKEND_INTERNAL)
-class OptionCreateAction(CreateAction):
- """
- (internal) Action to create an option
- """
-
- model = Option()
- schema = DefaultSchema(Option()).get_create_schema(
- required_properties=["meeting_id"],
- optional_properties=[
- "text",
- "poll_id",
- "used_as_global_option_in_poll_id",
- "content_object_id",
- "yes",
- "no",
- "abstain",
- "weight",
- ],
- additional_optional_fields={"poll_candidate_user_ids": id_list_schema},
- )
-
- def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
- keyword = self.check_one_of_three_keywords(instance)
- action_data = []
- user_token = get_user_token()
- yes_data = self.get_vote_action_data(instance, "Y", "yes", user_token)
- if yes_data is not None:
- action_data.append(yes_data)
- no_data = self.get_vote_action_data(instance, "N", "no", user_token)
- if no_data is not None:
- action_data.append(no_data)
- abstain_data = self.get_vote_action_data(instance, "A", "abstain", user_token)
- if abstain_data is not None:
- action_data.append(abstain_data)
- if action_data:
- self.apply_instance(instance)
- self.execute_other_action(VoteCreate, action_data)
- if keyword == "poll_candidate_user_ids":
- self.apply_instance(instance)
- user_ids = instance.pop("poll_candidate_user_ids")
- self.execute_other_action(
- PollCandidateListCreate,
- [
- {
- "option_id": instance["id"],
- "meeting_id": instance["meeting_id"],
- "entries": [
- {"user_id": user_id, "weight": i}
- for i, user_id in enumerate(user_ids, start=1)
- ],
- }
- ],
- )
-
- return instance
-
- def get_vote_action_data(
- self, instance: dict[str, Any], value: str, prop: str, user_token: str
- ) -> dict[str, Any] | None:
- if instance.get(prop):
- return {
- "value": value,
- "weight": instance[prop],
- "option_id": instance["id"],
- "user_token": user_token,
- }
- return None
-
- @staticmethod
- def check_one_of_three_keywords(instance: dict[str, Any]) -> str:
- keys = [
- key
- for key in ("text", "content_object_id", "poll_candidate_user_ids")
- if key in instance
- ]
- if len(keys) != 1:
- raise ActionException(
- "Need one of text, content_object_id or poll_candidate_user_ids."
- )
- return keys[0]
diff --git a/openslides_backend/action/actions/option/delete.py b/openslides_backend/action/actions/option/delete.py
deleted file mode 100644
index b2bc679e67..0000000000
--- a/openslides_backend/action/actions/option/delete.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from typing import Any
-
-from ....models.models import Option
-from ....shared.patterns import (
- collection_from_fqid,
- fqid_from_collection_and_id,
- id_from_fqid,
-)
-from ...generics.delete import DeleteAction
-from ...util.action_type import ActionType
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-from ..poll_candidate_list.delete import PollCandidateListDelete
-
-
-@register_action("option.delete", action_type=ActionType.BACKEND_INTERNAL)
-class OptionDelete(DeleteAction):
- """
- Action to delete options.
- """
-
- model = Option()
- schema = DefaultSchema(Option()).get_delete_schema()
-
- def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
- option = self.datastore.get(
- fqid_from_collection_and_id("option", instance["id"]), ["content_object_id"]
- )
- if (
- option.get("content_object_id")
- and collection_from_fqid(option["content_object_id"])
- == "poll_candidate_list"
- ):
- self.execute_other_action(
- PollCandidateListDelete,
- [{"id": id_from_fqid(option["content_object_id"])}],
- )
- return instance
diff --git a/openslides_backend/action/actions/option/set_auto_fields.py b/openslides_backend/action/actions/option/set_auto_fields.py
deleted file mode 100644
index f297d50e70..0000000000
--- a/openslides_backend/action/actions/option/set_auto_fields.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from typing import Any
-
-from ....models.models import Option
-from ...generics.update import UpdateAction
-from ...util.action_type import ActionType
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-
-
-@register_action("option.set_auto_fields", action_type=ActionType.BACKEND_INTERNAL)
-class OptionSetAutoFields(UpdateAction):
- """
- Action to calculate auto fields for options (yes, no, abstain)
- """
-
- model = Option()
- schema = DefaultSchema(Option()).get_update_schema(
- optional_properties=["yes", "no", "abstain"]
- )
-
- def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
- set_without_calc = (
- instance.get("yes") or instance.get("no") or instance.get("abstain")
- )
- if not set_without_calc:
- # TODO in this case we should autogenerate the fields.
- raise NotImplementedError()
- return instance
diff --git a/openslides_backend/action/actions/option/update.py b/openslides_backend/action/actions/option/update.py
deleted file mode 100644
index 9c1225d8d0..0000000000
--- a/openslides_backend/action/actions/option/update.py
+++ /dev/null
@@ -1,185 +0,0 @@
-from typing import Any
-
-from ....models.models import Option, Poll
-from ....services.database.commands import GetManyRequest
-from ....shared.exceptions import ActionException
-from ....shared.patterns import fqid_from_collection_and_id
-from ....shared.schema import decimal_schema
-from ...generics.update import UpdateAction
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-from ..poll.functions import check_poll_or_option_perms
-from ..poll.set_state import PollSetState
-from ..vote.create import VoteCreate
-from ..vote.update import VoteUpdate
-from ..vote.user_token_helper import get_user_token
-
-option_keys = ("yes", "no", "abstain")
-option_keys_map = {key[0].upper(): key for key in option_keys}
-
-
-@register_action("option.update")
-class OptionUpdateAction(UpdateAction):
- """
- Action to update an option.
- """
-
- model = Option()
- schema = DefaultSchema(Option()).get_update_schema(
- additional_optional_fields={
- "Y": decimal_schema,
- "N": decimal_schema,
- "A": decimal_schema,
- "publish_immediately": {"type": "boolean"},
- }
- )
-
- def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
- """Update votes and auto calculate yes, no, abstain."""
-
- option, poll = self._get_option_and_poll(instance["id"])
- state_change = self.check_state_change(instance, poll)
-
- if option.get("used_as_global_option_in_poll_id"):
- self._handle_global_option_data(instance, poll)
- else:
- self._handle_poll_option_data(instance, poll)
-
- id_to_vote = self._fetch_votes(option.get("vote_ids", []))
-
- action_data_create = []
- action_data_update = []
- user_token = get_user_token()
-
- for letter, option_key in option_keys_map.items():
- if option_key in instance:
- vote_id = self._get_vote_id(letter, id_to_vote)
- if vote_id is None:
- action_data_create.append(
- {
- "option_id": instance["id"],
- "value": letter,
- "weight": instance[option_key],
- "user_token": user_token,
- }
- )
- else:
- action_data_update.append(
- {"id": vote_id, "weight": instance[option_key]}
- )
- if action_data_create:
- self.execute_other_action(VoteCreate, action_data_create)
- if action_data_update:
- self.execute_other_action(VoteUpdate, action_data_update)
-
- execute_other_action = False
- if state_change:
- state = Poll.STATE_FINISHED
- execute_other_action = True
- if (
- execute_other_action
- or (
- poll["state"] == Poll.STATE_FINISHED
- and poll["type"] == Poll.TYPE_ANALOG
- )
- ) and instance.get("publish_immediately"):
- state = Poll.STATE_PUBLISHED
- execute_other_action = True
- if execute_other_action:
- self.execute_other_action(
- PollSetState, [{"id": poll["id"], "state": state}]
- )
-
- instance.pop("publish_immediately", None)
- return instance
-
- def _get_option_and_poll(
- self, option_id: int
- ) -> tuple[dict[str, Any], dict[str, Any]]:
- option = self.datastore.get(
- fqid_from_collection_and_id(self.model.collection, option_id),
- ["poll_id", "used_as_global_option_in_poll_id", "vote_ids", "meeting_id"],
- )
- return (
- option,
- self.datastore.get(
- fqid_from_collection_and_id("poll", option["poll_id"]),
- [
- "id",
- "state",
- "type",
- "pollmethod",
- "global_yes",
- "global_no",
- "global_abstain",
- "meeting_id",
- "content_object_id",
- ],
- lock_result=["type"],
- ),
- )
-
- def _handle_poll_option_data(
- self, instance: dict[str, Any], poll: dict[str, Any]
- ) -> None:
- if poll.get("type") == "analog":
- data = self._get_data(instance)
- pollmethod = poll["pollmethod"]
- for letter, key in option_keys_map.items():
- if letter in pollmethod:
- instance[key] = data.get(key, "-2.000000")
- elif data.get(key) is not None:
- raise ActionException(
- f"Pollmethod {pollmethod} does not support {key} votes"
- )
-
- def _handle_global_option_data(
- self, instance: dict[str, Any], poll: dict[str, Any]
- ) -> None:
- if poll.get("type") == "analog":
- data = self._get_data(instance)
- for key in option_keys:
- if poll.get(f"global_{key}") and poll.get("pollmethod") in ("Y", "N"):
- instance[key] = data.get(key, "-2.000000")
- elif key in data:
- raise ActionException(
- f"Global {key} votes are not allowed for this poll"
- )
-
- def _get_data(self, instance: dict[str, Any]) -> dict[str, Any]:
- return {
- key: instance.pop(letter)
- for letter, key in option_keys_map.items()
- if letter in instance
- }
-
- def _fetch_votes(self, vote_ids: list[int]) -> dict[int, dict[str, Any]]:
- get_many_request = GetManyRequest("vote", vote_ids, ["value"])
- gm_result = self.datastore.get_many([get_many_request])
- votes = gm_result.get("vote", {})
- return votes
-
- def _get_vote_id(
- self, search_value: str, id_to_vote: dict[int, dict[str, Any]]
- ) -> int | None:
- for key, item in id_to_vote.items():
- if item["value"] == search_value:
- return key
- return None
-
- def check_state_change(
- self, instance: dict[str, Any], poll: dict[str, Any]
- ) -> bool:
- return (
- poll.get("type") == Poll.TYPE_ANALOG
- and poll.get("state") == Poll.STATE_CREATED
- and any(letter in instance for letter in option_keys_map.keys())
- )
-
- def check_permissions(self, instance: dict[str, Any]) -> None:
- _, poll = self._get_option_and_poll(instance["id"])
- content_object_id = poll.get("content_object_id", "")
- meeting_id = poll["meeting_id"]
- check_poll_or_option_perms(
- content_object_id, self.datastore, self.user_id, meeting_id
- )
diff --git a/openslides_backend/action/actions/poll/__init__.py b/openslides_backend/action/actions/poll/__init__.py
deleted file mode 100644
index 4a66fcc507..0000000000
--- a/openslides_backend/action/actions/poll/__init__.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from . import ( # noqa
- anonymize,
- create,
- delete,
- publish,
- reset,
- set_state,
- start,
- stop,
- update,
-)
diff --git a/openslides_backend/action/actions/poll/anonymize.py b/openslides_backend/action/actions/poll/anonymize.py
deleted file mode 100644
index 280d020e31..0000000000
--- a/openslides_backend/action/actions/poll/anonymize.py
+++ /dev/null
@@ -1,76 +0,0 @@
-from typing import Any
-
-from openslides_backend.action.mixins.extend_history_mixin import ExtendHistoryMixin
-
-from ....models.models import Poll
-from ....services.database.commands import GetManyRequest
-from ....shared.exceptions import ActionException
-from ....shared.patterns import fqid_from_collection_and_id
-from ...generics.update import UpdateAction
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-from ...util.typing import ActionData
-from ..vote.anonymize import VoteAnonymize
-from .mixins import PollHistoryMixin, PollPermissionMixin
-
-
-@register_action("poll.anonymize")
-class PollAnonymize(
- ExtendHistoryMixin, UpdateAction, PollPermissionMixin, PollHistoryMixin
-):
- """
- Action to anonymize a poll.
- """
-
- model = Poll()
- schema = DefaultSchema(Poll()).get_update_schema()
- poll_history_information = "anonymized"
- extend_history_to = "content_object_id"
-
- def get_updated_instances(self, action_data: ActionData) -> ActionData:
- for instance in action_data:
- self.check_allowed(instance["id"])
- option_ids = self._get_option_ids(instance["id"])
- options = self._get_options(option_ids)
-
- for option_id in options:
- option = options[option_id]
- if option.get("vote_ids"):
- self._remove_user_id_from(option["vote_ids"])
-
- instance["is_pseudoanonymized"] = True
- yield instance
-
- def check_allowed(self, poll_id: int) -> None:
- poll = self.datastore.get(
- fqid_from_collection_and_id("poll", poll_id), ["type", "state"]
- )
-
- if not poll.get("state") in (Poll.STATE_FINISHED, Poll.STATE_PUBLISHED):
- raise ActionException(
- "Anonymizing can only be done after finishing a poll."
- )
- if poll.get("type") != Poll.TYPE_NAMED:
- raise ActionException("You can only anonymize named polls.")
-
- def _get_option_ids(self, poll_id: int) -> list[int]:
- poll = self.datastore.get(
- fqid_from_collection_and_id(self.model.collection, poll_id),
- ["option_ids", "global_option_id"],
- )
- option_ids = poll.get("option_ids", [])
- if poll.get("global_option_id"):
- option_ids.append(poll["global_option_id"])
- return option_ids
-
- def _get_options(self, option_ids: list[int]) -> dict[int, dict[str, Any]]:
- get_many_request = GetManyRequest("option", option_ids, ["vote_ids"])
- gm_result = self.datastore.get_many([get_many_request])
- options: dict[int, dict[str, Any]] = gm_result.get("option", {})
- return options
-
- def _remove_user_id_from(self, vote_ids: list[int]) -> None:
- action_data = []
- for id_ in vote_ids:
- action_data.append({"id": id_})
- self.execute_other_action(VoteAnonymize, action_data)
diff --git a/openslides_backend/action/actions/poll/base.py b/openslides_backend/action/actions/poll/base.py
deleted file mode 100644
index 62784f4d0f..0000000000
--- a/openslides_backend/action/actions/poll/base.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from openslides_backend.models.models import Poll
-
-from ....shared.exceptions import ActionException
-
-
-def base_check_onehundred_percent_base(
- pollmethod: str | None, onehundred_percent_base: str | None
-) -> None:
- error_msg = "This onehundred_percent_base not allowed in this pollmethod."
- if pollmethod == "Y" and onehundred_percent_base in (
- Poll.ONEHUNDRED_PERCENT_BASE_N,
- Poll.ONEHUNDRED_PERCENT_BASE_YN,
- Poll.ONEHUNDRED_PERCENT_BASE_YNA,
- ):
- raise ActionException(error_msg)
- elif pollmethod == "N" and onehundred_percent_base in (
- Poll.ONEHUNDRED_PERCENT_BASE_Y,
- Poll.ONEHUNDRED_PERCENT_BASE_YN,
- Poll.ONEHUNDRED_PERCENT_BASE_YNA,
- ):
- raise ActionException(error_msg)
- elif (
- pollmethod == "YN"
- and onehundred_percent_base == Poll.ONEHUNDRED_PERCENT_BASE_YNA
- ):
- raise ActionException(error_msg)
diff --git a/openslides_backend/action/actions/poll/create.py b/openslides_backend/action/actions/poll/create.py
deleted file mode 100644
index 0c8725c756..0000000000
--- a/openslides_backend/action/actions/poll/create.py
+++ /dev/null
@@ -1,252 +0,0 @@
-from typing import Any
-
-from openslides_backend.shared.util import ONE_ORGANIZATION_FQID
-
-from ....models.models import Poll
-from ....shared.exceptions import ActionException
-from ....shared.patterns import collection_from_fqid, fqid_from_collection_and_id
-from ....shared.schema import decimal_schema, id_list_schema, optional_fqid_schema
-from ...mixins.forbid_anonymous_group_mixin import ForbidAnonymousGroupMixin
-from ...mixins.sequential_numbers_mixin import SequentialNumbersMixin
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-from ..option.create import OptionCreateAction
-from .base import base_check_onehundred_percent_base
-from .mixins import PollHistoryMixin, PollPermissionMixin, PollValidationMixin
-
-options_schema = {
- "description": "A option inside a poll create schema",
- "type": "object",
- "properties": {
- "text": {"type": "string", "description": "the text of an option"},
- "content_object_id": optional_fqid_schema,
- "poll_candidate_user_ids": id_list_schema,
- "Y": decimal_schema,
- "N": decimal_schema,
- "A": decimal_schema,
- },
- "additionalProperties": False,
-}
-
-
-@register_action("poll.create")
-class PollCreateAction(
- PollValidationMixin,
- SequentialNumbersMixin,
- PollPermissionMixin,
- PollHistoryMixin,
- ForbidAnonymousGroupMixin,
-):
- """
- Action to create a poll.
- """
-
- model = Poll()
- schema = DefaultSchema(Poll()).get_create_schema(
- required_properties=["title", "type", "pollmethod", "meeting_id"],
- additional_required_fields={
- "options": {
- "type": "array",
- "items": options_schema,
- "minItems": 1,
- }
- },
- optional_properties=[
- "content_object_id",
- "description",
- "min_votes_amount",
- "max_votes_amount",
- "max_votes_per_option",
- "global_yes",
- "global_no",
- "global_abstain",
- "onehundred_percent_base",
- "votesvalid",
- "votesinvalid",
- "votescast",
- "entitled_group_ids",
- "backend",
- "live_voting_enabled",
- ],
- additional_optional_fields={
- "publish_immediately": {"type": "boolean"},
- "amount_global_yes": decimal_schema,
- "amount_global_no": decimal_schema,
- "amount_global_abstain": decimal_schema,
- },
- )
- poll_history_information = "created"
-
- def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
- instance = super().update_instance(instance)
- action_data = []
-
- state_change = self.check_state_change(instance)
- is_motion_poll = collection_from_fqid(instance["content_object_id"]) == "motion"
- is_assignment_poll = (
- collection_from_fqid(instance["content_object_id"]) == "assignment"
- )
-
- # check enabled_electronic_voting
- if instance["type"] in (Poll.TYPE_NAMED, Poll.TYPE_PSEUDOANONYMOUS):
- organization = self.datastore.get(
- ONE_ORGANIZATION_FQID,
- ["enable_electronic_voting"],
- )
- if not organization.get("enable_electronic_voting"):
- raise ActionException("Electronic voting is not allowed.")
-
- # check named and live_voting_enabled
- if instance.get("live_voting_enabled") and not (
- instance["type"] == Poll.TYPE_NAMED
- and (
- is_motion_poll
- or (
- is_assignment_poll
- and not instance.get("global_yes")
- and instance["pollmethod"] == "Y"
- and instance.get("max_votes_amount") == 1
- )
- )
- ):
- raise ActionException(
- "live_voting_enabled only allowed for named motion polls and named Yes assignment polls."
- )
-
- # check entitled_group_ids and analog
- if instance["type"] == Poll.TYPE_ANALOG and "entitled_group_ids" in instance:
- raise ActionException("entitled_group_ids is not allowed for analog.")
- # check analog and onehundredpercentbase entitled
- if instance["type"] == Poll.TYPE_ANALOG and (
- base := instance.get("onehundred_percent_base")
- ) in (
- Poll.ONEHUNDRED_PERCENT_BASE_ENTITLED,
- Poll.ONEHUNDRED_PERCENT_BASE_ENTITLED_PRESENT,
- ):
- raise ActionException(
- f"onehundred_percent_base: value {base} is not allowed for analog."
- )
- self.check_onehundred_percent_base(instance)
-
- # check non-analog and publish_immediately
- if instance["type"] != Poll.TYPE_ANALOG and "publish_immediately" in instance:
- raise ActionException("publish_immediately only allowed for analog polls.")
-
- # check content_object_id motion and state allow_create_poll
- if is_motion_poll:
- motion = self.datastore.get(instance["content_object_id"], ["state_id"])
- if not motion.get("state_id"):
- raise ActionException("Motion doesn't have a state.")
- state = self.datastore.get(
- fqid_from_collection_and_id("motion_state", motion["state_id"]),
- ["allow_create_poll"],
- )
- if not state.get("allow_create_poll"):
- raise ActionException("Motion state doesn't allow to create poll.")
-
- # handle non-global options
- unique_set = set()
-
- for weight, option in enumerate(instance.get("options", []), start=1):
- # check the keys with staticmethod from option.create, where they belong
- key = OptionCreateAction.check_one_of_three_keywords(option)
- data: dict[str, Any] = {
- "poll_id": instance["id"],
- "meeting_id": instance["meeting_id"],
- "weight": weight,
- key: option[key],
- }
-
- o_obj = f"{key},{option[key]}"
- if o_obj in unique_set:
- raise ActionException(
- f"Duplicated option in poll.options: {option[key]}"
- )
- else:
- unique_set.add(o_obj)
-
- if instance["type"] == "analog":
- if instance["pollmethod"] == "N":
- data["no"] = self.parse_vote_value(option, "N")
- else:
- data["yes"] = self.parse_vote_value(option, "Y")
- if instance["pollmethod"] in ("YN", "YNA"):
- data["no"] = self.parse_vote_value(option, "N")
- if instance["pollmethod"] == "YNA":
- data["abstain"] = self.parse_vote_value(option, "A")
-
- action_data.append(data)
-
- # handle global option
- global_data = {
- "text": "global option",
- "used_as_global_option_in_poll_id": instance["id"],
- "meeting_id": instance["meeting_id"],
- "weight": 1,
- }
- if instance["type"] == "analog":
- for option in ["yes", "no", "abstain"]:
- if instance["type"] == "analog" and instance.get(f"global_{option}"):
- global_data[option] = self.parse_vote_value(
- instance, f"amount_global_{option}"
- )
- instance.pop(f"amount_global_{option}", None)
- action_data.append(global_data)
-
- # Execute the create option actions
- self.apply_instance(instance)
- self.execute_other_action(
- OptionCreateAction,
- action_data,
- )
-
- # set state
- instance["state"] = Poll.STATE_CREATED
- if state_change:
- instance["state"] = Poll.STATE_FINISHED
- if (
- instance["type"] == Poll.TYPE_ANALOG
- and instance["state"] == Poll.STATE_FINISHED
- and instance.get("publish_immediately")
- ):
- instance["state"] = Poll.STATE_PUBLISHED
-
- # set votescast, votesvalid, votesinvalid defaults
- for field in ("votescast", "votesvalid", "votesinvalid"):
- instance[field] = (
- self.parse_vote_value(instance, field)
- if instance["type"] == Poll.TYPE_ANALOG
- else instance.get(field, "0.000000")
- )
-
- # calculate is_pseudoanonymized
- instance["is_pseudoanonymized"] = instance["type"] == Poll.TYPE_PSEUDOANONYMOUS
-
- instance.pop("options", None)
- instance.pop("publish_immediately", None)
- self.check_anonymous_not_in_list_fields(instance, ["entitled_group_ids"])
- return instance
-
- def parse_vote_value(self, data: dict[str, Any], field: str) -> Any:
- return data.get(field, "-2.000000")
-
- def check_onehundred_percent_base(self, instance: dict[str, Any]) -> None:
- pollmethod = instance["pollmethod"]
- onehundred_percent_base = instance.get("onehundred_percent_base")
- base_check_onehundred_percent_base(pollmethod, onehundred_percent_base)
-
- def check_state_change(self, instance: dict[str, Any]) -> bool:
- if instance["type"] != Poll.TYPE_ANALOG:
- return False
- check_fields = (
- "votesvalid",
- "votesinvalid",
- "votescast",
- )
- for field in check_fields:
- if instance.get(field):
- return True
- for option in instance.get("options", []):
- if option.get("Y") or option.get("N") or option.get("A"):
- return True
- return False
diff --git a/openslides_backend/action/actions/poll/delete.py b/openslides_backend/action/actions/poll/delete.py
deleted file mode 100644
index 127617011b..0000000000
--- a/openslides_backend/action/actions/poll/delete.py
+++ /dev/null
@@ -1,103 +0,0 @@
-from collections.abc import Callable
-
-from ....models.models import Poll
-from ....services.database.interface import GetManyRequest
-from ....shared.exceptions import VoteServiceException
-from ...generics.delete import DeleteAction
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-from ...util.typing import ActionData
-from .mixins import PollHistoryMixin, PollPermissionMixin
-
-
-@register_action("poll.delete")
-class PollDelete(DeleteAction, PollPermissionMixin, PollHistoryMixin):
- """
- Action to delete polls.
- """
-
- model = Poll()
- schema = DefaultSchema(Poll()).get_delete_schema()
- poll_history_information = "deleted"
-
- def prefetch(self, action_data: ActionData) -> None:
- result = self.datastore.get_many(
- [
- GetManyRequest(
- "poll",
- list({instance["id"] for instance in action_data}),
- [
- "content_object_id",
- "meeting_id",
- "entitled_group_ids",
- "voted_ids",
- "option_ids",
- "global_option_id",
- "projection_ids",
- "state",
- ],
- ),
- ],
- use_changed_models=False,
- )
- polls = result["poll"].values()
- self.started_polls = [
- id_
- for id_, poll in result["poll"].items()
- if poll.get("state") == "started"
- ]
- meeting_ids = list({poll["meeting_id"] for poll in polls})
- group_ids = list(
- {
- group_id
- for poll in polls
- for group_id in poll.get("entitled_group_ids", ())
- }
- )
- option_ids = [
- option_id
- for poll in polls
- if poll.get("option_ids")
- for option_id in poll["option_ids"]
- ]
- requests = [
- GetManyRequest(
- "meeting",
- meeting_ids,
- [
- "is_active_in_organization_id",
- "name",
- "option_ids",
- "poll_ids",
- ],
- ),
- GetManyRequest(
- "option",
- option_ids,
- [
- "meeting_id",
- "vote_ids",
- "content_object_id",
- "poll_id",
- "used_as_global_option_in_poll_id",
- "vote_ids",
- ],
- ),
- GetManyRequest(
- "group",
- group_ids,
- ["poll_ids"],
- ),
- ]
- self.datastore.get_many(requests, use_changed_models=False)
-
- def get_on_success(self, action_data: ActionData) -> Callable[[], None]:
- def on_success() -> None:
- for instance in action_data:
- if (id_ := instance["id"]) in self.started_polls:
- try:
- self.vote_service.clear(id_)
- except VoteServiceException as e:
- self.logger.error(f"Error clearing vote {id_}: {str(e)}")
-
- return on_success
diff --git a/openslides_backend/action/actions/poll/functions.py b/openslides_backend/action/actions/poll/functions.py
deleted file mode 100644
index 8899611043..0000000000
--- a/openslides_backend/action/actions/poll/functions.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from ....permissions.permission_helper import has_perm
-from ....permissions.permissions import Permission, Permissions
-from ....services.database.interface import Database
-from ....shared.exceptions import ActionException, MissingPermission
-from ....shared.patterns import KEYSEPARATOR, collection_from_fqid
-
-
-def check_poll_or_option_perms(
- content_object_id: str,
- datastore: Database,
- user_id: int,
- meeting_id: int,
-) -> None:
- if content_object_id.startswith("motion" + KEYSEPARATOR):
- perm: Permission = Permissions.Motion.CAN_MANAGE_POLLS
- elif content_object_id.startswith("assignment" + KEYSEPARATOR):
- perm = Permissions.Assignment.CAN_MANAGE_POLLS
- elif content_object_id.startswith("topic" + KEYSEPARATOR):
- perm = Permissions.Poll.CAN_MANAGE
- else:
- raise ActionException(
- f"'{collection_from_fqid(content_object_id)}' is not a valid poll collection."
- )
- if not has_perm(datastore, user_id, perm, meeting_id):
- raise MissingPermission(perm)
diff --git a/openslides_backend/action/actions/poll/mixins.py b/openslides_backend/action/actions/poll/mixins.py
deleted file mode 100644
index 5191925d08..0000000000
--- a/openslides_backend/action/actions/poll/mixins.py
+++ /dev/null
@@ -1,259 +0,0 @@
-from collections import defaultdict
-from decimal import Decimal
-from typing import Any, cast
-
-from psycopg.types.json import Jsonb
-
-from openslides_backend.shared.typing import HistoryInformation
-
-from ....services.database.commands import GetManyRequest
-from ....shared.exceptions import ActionException, VoteServiceException
-from ....shared.patterns import collection_from_fqid, fqid_from_collection_and_id
-from ...action import Action
-from ..option.set_auto_fields import OptionSetAutoFields
-from ..projector_countdown.mixins import CountdownCommand, CountdownControl
-from ..vote.create import VoteCreate
-from ..vote.user_token_helper import get_user_token
-from .functions import check_poll_or_option_perms
-
-
-class PollValidationMixin(Action):
- def validate_instance(self, instance: dict[str, Any]) -> None:
- super().validate_instance(instance)
-
- if poll_id := instance.get("id"):
- poll = self.datastore.get(
- fqid_from_collection_and_id("poll", poll_id),
- ["max_votes_amount", "min_votes_amount", "max_votes_per_option"],
- )
- max_votes_amount = cast(
- int,
- instance.get(
- "max_votes_amount", poll["max_votes_amount"] if poll_id else 1
- ),
- )
- min_votes_amount = cast(
- int,
- instance.get(
- "min_votes_amount", poll["min_votes_amount"] if poll_id else 1
- ),
- )
- max_votes_per_option = cast(
- int,
- instance.get(
- "max_votes_per_option", poll["max_votes_per_option"] if poll_id else 1
- ),
- )
-
- if max_votes_amount < max_votes_per_option:
- raise ActionException(
- "The maximum votes per option cannot be higher than the maximum amount of votes in total."
- )
- if max_votes_amount < min_votes_amount:
- raise ActionException(
- "The minimum amount of votes cannot be higher than the maximum amount of votes."
- )
-
-
-class PollPermissionMixin(Action):
- def check_permissions(self, instance: dict[str, Any]) -> None:
- if "meeting_id" in instance:
- content_object_id = instance.get("content_object_id", "")
- meeting_id = instance["meeting_id"]
- else:
- poll = self.datastore.get(
- fqid_from_collection_and_id("poll", instance["id"]),
- ["content_object_id", "meeting_id"],
- lock_result=False,
- )
- content_object_id = poll.get("content_object_id", "")
- meeting_id = poll["meeting_id"]
- if not content_object_id:
- raise ActionException("No 'content_object_id' was given")
- check_poll_or_option_perms(
- content_object_id, self.datastore, self.user_id, meeting_id
- )
-
-
-class StopControl(CountdownControl, Action):
- def on_stop(self, instance: dict[str, Any]) -> None:
- poll = self.datastore.get(
- fqid_from_collection_and_id(self.model.collection, instance["id"]),
- [
- "state",
- "meeting_id",
- "pollmethod",
- "global_option_id",
- "entitled_group_ids",
- ],
- )
- # reset countdown given by meeting
- meeting = self.datastore.get(
- fqid_from_collection_and_id("meeting", poll["meeting_id"]),
- [
- "poll_couple_countdown",
- "poll_countdown_id",
- "users_enable_vote_weight",
- "users_enable_vote_delegations",
- ],
- )
- if meeting.get("poll_couple_countdown") and meeting.get("poll_countdown_id"):
- self.control_countdown(meeting["poll_countdown_id"], CountdownCommand.RESET)
-
- # stop poll in vote service and create vote objects
- results = self.vote_service.stop(instance["id"])
- action_data = []
- votesvalid = Decimal("0.000000")
- option_results: dict[int, dict[str, Decimal]] = defaultdict(
- lambda: defaultdict(lambda: Decimal("0.000000"))
- ) # maps options to their respective YNA sums
- for ballot in results["votes"]:
- user_token = get_user_token()
- vote_weight = Decimal(ballot["weight"])
- votesvalid += vote_weight
- vote_template: dict[str, str | int] = {"user_token": user_token}
- if "vote_user_id" in ballot:
- vote_template["user_id"] = ballot["vote_user_id"]
- if "request_user_id" in ballot:
- vote_template["delegated_user_id"] = ballot["request_user_id"]
-
- if isinstance(ballot["value"], dict):
- for option_id_str, value in ballot["value"].items():
- option_id = int(option_id_str)
-
- vote_value = value
- vote_weighted = vote_weight # use new variable vote_weighted because pollmethod=Y/N does not imply anymore that only one loop is done (see max_votes_per_option)
- if poll["pollmethod"] in ("Y", "N"):
- if value == 0:
- continue
- vote_value = poll["pollmethod"]
- vote_weighted *= value
-
- option_results[option_id][vote_value] += vote_weighted
- action_data.append(
- {
- "value": vote_value,
- "option_id": option_id,
- "weight": str(vote_weighted),
- **vote_template,
- }
- )
- elif isinstance(ballot["value"], str):
- vote_value = ballot["value"]
- option_id = poll["global_option_id"]
- option_results[option_id][vote_value] += vote_weight
- action_data.append(
- {
- "value": vote_value,
- "option_id": option_id,
- "weight": str(vote_weight),
- **vote_template,
- }
- )
- else:
- raise VoteServiceException("Invalid response from vote service")
- self.execute_other_action(VoteCreate, action_data)
- # update results into option
- self.execute_other_action(
- OptionSetAutoFields,
- [
- {
- "id": _id,
- "yes": str(option["Y"]),
- "no": str(option["N"]),
- "abstain": str(option["A"]),
- }
- for _id, option in option_results.items()
- ],
- )
- # set voted ids
- voted_ids = results["user_ids"]
- instance["voted_ids"] = voted_ids
-
- # set votescast, votesvalid, votesinvalid
- instance["votesvalid"] = str(votesvalid)
- instance["votescast"] = str(Decimal("0.000000") + Decimal(len(voted_ids)))
- instance["votesinvalid"] = "0.000000"
-
- # set entitled users at stop.
- instance["entitled_users_at_stop"] = Jsonb(
- self.get_entitled_users(poll | instance, meeting)
- )
-
- def get_entitled_users(
- self, poll: dict[str, Any], meeting: dict[str, Any]
- ) -> list[dict[str, Any]]:
- entitled_users = []
- all_voted_users = set(poll.get("voted_ids", []))
-
- # get all users from the groups.
- gmr = GetManyRequest(
- "group", poll.get("entitled_group_ids", []), ["meeting_user_ids"]
- )
- gm_result = self.datastore.get_many([gmr])
- groups = gm_result.get("group", {}).values()
-
- # fetch presence status
- meeting_user_ids = set()
- for group in groups:
- meeting_user_ids.update(group.get("meeting_user_ids", []))
- gmr = GetManyRequest(
- "meeting_user", list(meeting_user_ids), ["user_id", "vote_delegated_to_id"]
- )
- gm_result = self.datastore.get_many([gmr])
- meeting_users = gm_result.get("meeting_user", {}).values()
-
- mu_to_user_id = {}
- if meeting.get("users_enable_vote_delegations"):
- # fetch vote delegations
- delegated_to_mu_ids = list(
- {id_ for mu in meeting_users if (id_ := mu.get("vote_delegated_to_id"))}
- )
- if delegated_to_mu_ids:
- gmr = GetManyRequest("meeting_user", delegated_to_mu_ids, ["user_id"])
- mu_to_user_id = self.datastore.get_many([gmr]).get("meeting_user", {})
-
- gmr = GetManyRequest(
- "user",
- [mu["user_id"] for mu in meeting_users],
- ["is_present_in_meeting_ids"],
- )
- users = self.datastore.get_many([gmr]).get("user", {})
-
- for mu in meeting_users:
- entitled_users.append(
- {
- "voted": mu["user_id"] in all_voted_users,
- "present": poll["meeting_id"]
- in users[mu["user_id"]].get("is_present_in_meeting_ids", []),
- "user_id": mu["user_id"],
- "vote_delegated_to_user_id": (
- mu_to_user_id[vote_mu_id]["user_id"]
- if (vote_mu_id := mu.get("vote_delegated_to_id"))
- and meeting.get("users_enable_vote_delegations")
- else None
- ),
- }
- )
-
- return entitled_users
-
-
-class PollHistoryMixin(Action):
- poll_history_information: str
-
- def get_history_information(self) -> HistoryInformation | None:
- # no datastore access necessary if information is in payload
- polls = self.get_instances_with_fields(["content_object_id"])
- return {
- poll["content_object_id"]: [
- f"{self.get_history_title(poll)} {self.poll_history_information}"
- ]
- for poll in polls
- }
-
- def get_history_title(self, poll: dict[str, Any]) -> str:
- content_collection = collection_from_fqid(poll["content_object_id"])
- if content_collection == "assignment":
- return "Ballot"
- return "Voting"
diff --git a/openslides_backend/action/actions/poll/publish.py b/openslides_backend/action/actions/poll/publish.py
deleted file mode 100644
index 972820483a..0000000000
--- a/openslides_backend/action/actions/poll/publish.py
+++ /dev/null
@@ -1,53 +0,0 @@
-from typing import Any
-
-from openslides_backend.action.mixins.extend_history_mixin import ExtendHistoryMixin
-from openslides_backend.shared.typing import HistoryInformation
-
-from ....models.models import Poll
-from ....shared.exceptions import ActionException
-from ....shared.patterns import fqid_from_collection_and_id
-from ...generics.update import UpdateAction
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-from .mixins import PollHistoryMixin, PollPermissionMixin, StopControl
-
-
-@register_action("poll.publish")
-class PollPublishAction(
- ExtendHistoryMixin,
- StopControl,
- UpdateAction,
- PollPermissionMixin,
- PollHistoryMixin,
-):
- """
- Action to publish a poll.
- """
-
- model = Poll()
- schema = DefaultSchema(Poll()).get_update_schema()
- extend_history_to = "content_object_id"
-
- def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
- poll = self.datastore.get(
- fqid_from_collection_and_id(self.model.collection, instance["id"]),
- ["state"],
- )
- if poll.get("state") not in [Poll.STATE_FINISHED, Poll.STATE_STARTED]:
- raise ActionException(
- f"Cannot publish poll {instance['id']}, because it is not in state finished or started."
- )
- if poll["state"] == Poll.STATE_STARTED:
- self.on_stop(instance)
-
- instance["state"] = Poll.STATE_PUBLISHED
- return instance
-
- def get_history_information(self) -> HistoryInformation | None:
- polls = self.get_instances_with_fields(["content_object_id", "state"])
- return {
- poll["content_object_id"]: [
- f"{self.get_history_title(poll)} {'stopped/' if poll['state'] != Poll.STATE_FINISHED else ''}published"
- ]
- for poll in polls
- }
diff --git a/openslides_backend/action/actions/poll/reset.py b/openslides_backend/action/actions/poll/reset.py
deleted file mode 100644
index 3a71209753..0000000000
--- a/openslides_backend/action/actions/poll/reset.py
+++ /dev/null
@@ -1,138 +0,0 @@
-from collections.abc import Callable
-from typing import Any
-
-from openslides_backend.action.mixins.extend_history_mixin import ExtendHistoryMixin
-
-from ....models.models import Poll
-from ....services.database.interface import GetManyRequest
-from ....shared.patterns import fqid_from_collection_and_id
-from ...generics.update import UpdateAction
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-from ...util.typing import ActionData
-from ..option.set_auto_fields import OptionSetAutoFields
-from ..vote.delete import VoteDelete
-from .mixins import PollHistoryMixin, PollPermissionMixin
-
-
-@register_action("poll.reset")
-class PollResetAction(
- ExtendHistoryMixin, UpdateAction, PollPermissionMixin, PollHistoryMixin
-):
- """
- Action to reset a poll.
- """
-
- model = Poll()
- schema = DefaultSchema(Poll()).get_update_schema()
- poll_history_information = "reset"
- extend_history_to = "content_object_id"
-
- def prefetch(self, action_data: ActionData) -> None:
- result = self.datastore.get_many(
- [
- GetManyRequest(
- "poll",
- list({instance["id"] for instance in action_data}),
- [
- "content_object_id",
- "meeting_id",
- "type",
- "voted_ids",
- "option_ids",
- "global_option_id",
- ],
- ),
- ],
- use_changed_models=False,
- )
- polls = result["poll"].values()
- meeting_ids = list({poll["meeting_id"] for poll in polls})
- option_ids = [
- option_id
- for poll in polls
- if poll.get("option_ids")
- for option_id in poll["option_ids"]
- ]
- requests = [
- GetManyRequest(
- "meeting",
- meeting_ids,
- [
- "is_active_in_organization_id",
- "name",
- ],
- ),
- GetManyRequest(
- "option",
- option_ids,
- ["vote_ids"],
- ),
- ]
- self.datastore.get_many(requests, use_changed_models=False)
-
- def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
- instance["state"] = Poll.STATE_CREATED
- self.delete_all_votes(instance["id"])
- poll = self.datastore.get(
- fqid_from_collection_and_id(self.model.collection, instance["id"]), ["type"]
- )
- instance["is_pseudoanonymized"] = poll.get("type") == Poll.TYPE_PSEUDOANONYMOUS
- instance["voted_ids"] = []
- instance["entitled_users_at_stop"] = None
- instance["votesvalid"] = None
- instance["votesinvalid"] = None
- instance["votescast"] = None
- return instance
-
- def delete_all_votes(self, poll_id: int) -> None:
- option_ids = self._get_option_ids(poll_id)
- options = self._get_options(option_ids)
- for option_id in options:
- option = options[option_id]
- if option.get("vote_ids"):
- self._delete_votes(option["vote_ids"])
- self._clear_option_auto_fields(option_id)
-
- def _get_option_ids(self, poll_id: int) -> list[int]:
- poll = self.datastore.get(
- fqid_from_collection_and_id(self.model.collection, poll_id),
- ["option_ids", "global_option_id"],
- )
- option_ids = poll.get("option_ids", [])
- if poll.get("global_option_id"):
- option_ids.append(poll["global_option_id"])
- return option_ids
-
- def _get_options(self, option_ids: list[int]) -> dict[int, dict[str, Any]]:
- get_many_request = GetManyRequest("option", option_ids, ["vote_ids"])
- gm_result = self.datastore.get_many(
- [get_many_request], use_changed_models=False
- )
- options: dict[int, dict[str, Any]] = gm_result.get("option", {})
-
- return options
-
- def _delete_votes(self, vote_ids: list[int]) -> None:
- action_data = []
- for id_ in vote_ids:
- action_data.append({"id": id_})
- self.execute_other_action(VoteDelete, action_data)
-
- def _clear_option_auto_fields(self, option_id: int) -> None:
- action_data = [
- {
- "id": option_id,
- "yes": "0.000000",
- "no": "0.000000",
- "abstain": "0.000000",
- }
- ]
- self.execute_other_action(OptionSetAutoFields, action_data)
-
- def get_on_success(self, action_data: ActionData) -> Callable[[], None]:
- def on_success() -> None:
- for instance in action_data:
- self.vote_service.clear(instance["id"])
-
- return on_success
diff --git a/openslides_backend/action/actions/poll/set_state.py b/openslides_backend/action/actions/poll/set_state.py
deleted file mode 100644
index 997ff8ccae..0000000000
--- a/openslides_backend/action/actions/poll/set_state.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from ....models.models import Poll
-from ...generics.update import UpdateAction
-from ...util.action_type import ActionType
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-
-
-@register_action("poll.set_state", action_type=ActionType.BACKEND_INTERNAL)
-class PollSetState(UpdateAction):
- """
- Internal action to set state of a poll.
- """
-
- model = Poll()
- schema = DefaultSchema(Poll()).get_update_schema(
- required_properties=["state"],
- )
diff --git a/openslides_backend/action/actions/poll/start.py b/openslides_backend/action/actions/poll/start.py
deleted file mode 100644
index 394c35b281..0000000000
--- a/openslides_backend/action/actions/poll/start.py
+++ /dev/null
@@ -1,74 +0,0 @@
-from collections.abc import Callable
-from typing import Any
-
-from openslides_backend.action.mixins.extend_history_mixin import ExtendHistoryMixin
-
-from ....models.models import Poll
-from ....shared.exceptions import ActionException, VoteServiceException
-from ....shared.patterns import fqid_from_collection_and_id
-from ...generics.update import UpdateAction
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-from ...util.typing import ActionData
-from ..projector_countdown.mixins import CountdownCommand, CountdownControl
-from .mixins import PollHistoryMixin, PollPermissionMixin
-
-
-@register_action("poll.start")
-class PollStartAction(
- ExtendHistoryMixin,
- CountdownControl,
- UpdateAction,
- PollPermissionMixin,
- PollHistoryMixin,
-):
- """
- Action to start a poll.
- """
-
- model = Poll()
- schema = DefaultSchema(Poll()).get_update_schema()
- poll_history_information = "started"
- extend_history_to = "content_object_id"
-
- def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
- poll = self.datastore.get(
- fqid_from_collection_and_id(self.model.collection, instance["id"]),
- ["state", "meeting_id", "type"],
- )
- if poll.get("type") == Poll.TYPE_ANALOG:
- raise ActionException(
- "Analog polls cannot be started. Please use poll.update instead to give votes."
- )
- if poll.get("state") != Poll.STATE_CREATED:
- raise ActionException(
- f"Cannot start poll {instance['id']}, because it is not in state created."
- )
- instance["state"] = Poll.STATE_STARTED
-
- # restart projector countdown given by the meeting
- meeting = self.datastore.get(
- fqid_from_collection_and_id("meeting", poll["meeting_id"]),
- [
- "poll_couple_countdown",
- "poll_countdown_id",
- ],
- )
- if meeting.get("poll_couple_countdown") and meeting.get("poll_countdown_id"):
- self.control_countdown(
- meeting["poll_countdown_id"], CountdownCommand.RESTART
- )
-
- self.vote_service.start(instance["id"])
-
- return instance
-
- def get_on_failure(self, action_data: ActionData) -> Callable[[], None]:
- def on_failure() -> None:
- for instance in action_data:
- try:
- self.vote_service.clear(instance["id"])
- except VoteServiceException as e:
- self.logger.error(f"Error clearing vote {instance['id']}: {str(e)}")
-
- return on_failure
diff --git a/openslides_backend/action/actions/poll/stop.py b/openslides_backend/action/actions/poll/stop.py
deleted file mode 100644
index e1884b7c6e..0000000000
--- a/openslides_backend/action/actions/poll/stop.py
+++ /dev/null
@@ -1,133 +0,0 @@
-from collections.abc import Callable
-from typing import Any
-
-from openslides_backend.action.mixins.extend_history_mixin import ExtendHistoryMixin
-
-from ....models.models import Poll
-from ....services.database.commands import GetManyRequest
-from ....shared.exceptions import ActionException, VoteServiceException
-from ....shared.patterns import fqid_from_collection_and_id
-from ...generics.update import UpdateAction
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-from ...util.typing import ActionData
-from .mixins import PollHistoryMixin, PollPermissionMixin, StopControl
-
-
-@register_action("poll.stop")
-class PollStopAction(
- ExtendHistoryMixin,
- StopControl,
- UpdateAction,
- PollPermissionMixin,
- PollHistoryMixin,
-):
- """
- Action to stop a poll.
- """
-
- model = Poll()
- schema = DefaultSchema(Poll()).get_update_schema()
- poll_history_information = "stopped"
- extend_history_to = "content_object_id"
-
- def prefetch(self, action_data: ActionData) -> None:
- result = self.datastore.get_many(
- [
- GetManyRequest(
- "poll",
- list({instance["id"] for instance in action_data}),
- [
- "content_object_id",
- "meeting_id",
- "state",
- "voted_ids",
- "pollmethod",
- "global_option_id",
- "entitled_group_ids",
- ],
- ),
- ],
- use_changed_models=False,
- )
- polls = result["poll"].values()
- meeting_ids = list({poll["meeting_id"] for poll in polls})
- requests = [
- GetManyRequest(
- "meeting",
- meeting_ids,
- [
- "poll_couple_countdown",
- "poll_countdown_id",
- "users_enable_vote_weight",
- "vote_ids",
- ],
- ),
- GetManyRequest(
- "group",
- list(
- {
- group_id
- for poll in polls
- for group_id in poll.get("entitled_group_ids", [])
- }
- ),
- ["meeting_user_ids"],
- ),
- ]
- result = self.datastore.get_many(requests, use_changed_models=False)
- groups = result["group"].values()
- result = self.datastore.get_many(
- [
- GetManyRequest(
- "meeting_user",
- list(
- {
- meeting_user_id
- for group in groups
- for meeting_user_id in group.get("meeting_user_ids", [])
- }
- ),
- ["user_id"],
- ),
- ],
- use_changed_models=False,
- )
- meeting_users = result["meeting_user"].values()
- self.datastore.get_many(
- [
- GetManyRequest(
- "user",
- list({mu["user_id"] for mu in meeting_users}),
- [
- "poll_voted_ids",
- "delegated_vote_ids",
- "vote_ids",
- ],
- ),
- ],
- use_changed_models=False,
- )
-
- def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
- poll = self.datastore.get(
- fqid_from_collection_and_id(self.model.collection, instance["id"]),
- ["state", "meeting_id", "voted_ids"],
- )
- if poll.get("state") != Poll.STATE_STARTED:
- raise ActionException(
- f"Cannot stop poll {instance['id']}, because it is not in state started."
- )
- instance["state"] = Poll.STATE_FINISHED
- self.on_stop(instance)
- return instance
-
- def get_on_success(self, action_data: ActionData) -> Callable[[], None]:
- def on_success() -> None:
- for instance in action_data:
- try:
- self.vote_service.clear(instance["id"])
- except VoteServiceException as e:
- self.logger.error(f"Error clearing vote {instance['id']}: {str(e)}")
-
- return on_success
diff --git a/openslides_backend/action/actions/poll/update.py b/openslides_backend/action/actions/poll/update.py
deleted file mode 100644
index bf0c49cb43..0000000000
--- a/openslides_backend/action/actions/poll/update.py
+++ /dev/null
@@ -1,252 +0,0 @@
-from typing import Any
-
-from psycopg.types.json import Jsonb
-
-from openslides_backend.action.mixins.extend_history_mixin import ExtendHistoryMixin
-from openslides_backend.services.database.interface import PartialModel
-
-from ....models.models import Poll
-from ....shared.exceptions import ActionException
-from ....shared.patterns import collection_from_fqid, fqid_from_collection_and_id
-from ...generics.update import UpdateAction
-from ...mixins.forbid_anonymous_group_mixin import ForbidAnonymousGroupMixin
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-from .base import base_check_onehundred_percent_base
-from .mixins import PollHistoryMixin, PollPermissionMixin, PollValidationMixin
-
-
-@register_action("poll.update")
-class PollUpdateAction(
- PollValidationMixin,
- ExtendHistoryMixin,
- UpdateAction,
- PollPermissionMixin,
- PollHistoryMixin,
- ForbidAnonymousGroupMixin,
-):
- """
- Action to update a poll.
- """
-
- internal_fields = [
- "entitled_users_at_stop",
- ]
-
- model = Poll()
- schema = DefaultSchema(Poll()).get_update_schema(
- optional_properties=[
- "pollmethod",
- "min_votes_amount",
- "max_votes_amount",
- "max_votes_per_option",
- "global_yes",
- "global_no",
- "global_abstain",
- "entitled_group_ids",
- "title",
- "description",
- "onehundred_percent_base",
- "votesvalid",
- "votesinvalid",
- "votescast",
- "backend",
- "live_voting_enabled",
- *internal_fields,
- ],
- additional_optional_fields={
- "publish_immediately": {"type": "boolean"},
- },
- )
- poll_history_information = "updated"
- extend_history_to = "content_object_id"
-
- def validate_fields(self, instance: dict[str, Any]) -> dict[str, Any]:
- super().validate_instance(instance)
- if not self.internal and any(
- forbidden := {key for key in instance if key in self.internal_fields}
- ):
- raise ActionException(f"data must not contain {forbidden} properties")
- return super().validate_fields(instance)
-
- def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
- poll = self.datastore.get(
- fqid_from_collection_and_id(self.model.collection, instance["id"]),
- [
- "state",
- "type",
- "entitled_users_at_stop",
- "content_object_id",
- "pollmethod",
- "max_votes_amount",
- "global_yes",
- ],
- )
-
- self.check_entitled_users_at_stop(instance, poll)
-
- state_change = self.check_state_change(instance, poll)
-
- self.check_onehundred_percent_base(instance)
-
- not_allowed = []
- if not poll.get("state") == Poll.STATE_CREATED:
- for key in (
- "pollmethod",
- "type",
- "min_votes_amount",
- "max_votes_amount",
- "max_votes_per_option",
- "global_yes",
- "global_no",
- "global_abstain",
- "backend",
- "live_voting_enabled",
- ):
- if key in instance:
- not_allowed.append(key)
- if (
- poll.get("state") != Poll.STATE_CREATED
- or poll.get("type") == Poll.TYPE_ANALOG
- ):
- if "entitled_group_ids" in instance:
- not_allowed.append("entitled_group_ids")
- if not poll.get("type") == Poll.TYPE_ANALOG:
- for key in (
- "votesvalid",
- "votesinvalid",
- "votescast",
- "publish_immediately",
- ):
- if key in instance:
- not_allowed.append(key)
-
- if not_allowed:
- raise ActionException(
- "Following options are not allowed in this state and type: "
- + ", ".join(not_allowed)
- )
-
- # check named and live_voting_enabled
- global_yes_for_check = instance.get("global_yes") or poll.get("global_yes")
- pollmethod_for_check = instance.get("pollmethod") or poll.get("pollmethod")
- max_votes_amount_for_check = instance.get("max_votes_amount") or poll.get(
- "max_votes_amount"
- )
- if instance.get("live_voting_enabled") and not (
- poll["type"] == Poll.TYPE_NAMED
- and (
- collection_from_fqid(poll["content_object_id"]) == "motion"
- or (
- collection_from_fqid(poll["content_object_id"]) == "assignment"
- and not global_yes_for_check
- and pollmethod_for_check == "Y"
- and max_votes_amount_for_check == 1
- )
- )
- ):
- raise ActionException(
- "live_voting_enabled only allowed for named motion polls and named Yes assignment polls."
- )
-
- if poll["type"] == Poll.TYPE_ANALOG and (
- base := instance.get("onehundred_percent_base")
- ) in (
- Poll.ONEHUNDRED_PERCENT_BASE_ENTITLED,
- Poll.ONEHUNDRED_PERCENT_BASE_ENTITLED_PRESENT,
- ):
- raise ActionException(
- f"onehundred_percent_base: value {base} is not allowed for analog."
- )
- if state_change:
- instance["state"] = Poll.STATE_FINISHED
- if (
- poll["type"] == Poll.TYPE_ANALOG
- and (
- instance.get("state") == Poll.STATE_FINISHED
- or poll["state"] == Poll.STATE_FINISHED
- )
- and instance.get("publish_immediately")
- ):
- instance["state"] = Poll.STATE_PUBLISHED
-
- # set votescast, votesvalid, votesinvalid defaults
- if poll["type"] == Poll.TYPE_ANALOG:
- for field in ("votescast", "votesvalid", "votesinvalid"):
- if field not in instance:
- instance[field] = "-2.000000"
-
- instance.pop("publish_immediately", None)
- self.check_anonymous_not_in_list_fields(instance, ["entitled_group_ids"])
-
- if raw_entitled_users_at_stop := instance.get("entitled_users_at_stop"):
- if isinstance(raw_entitled_users_at_stop, list):
- instance["entitled_users_at_stop"] = Jsonb(raw_entitled_users_at_stop)
-
- return instance
-
- def check_onehundred_percent_base(self, instance: dict[str, Any]) -> None:
- onehundred_percent_base = instance.get("onehundred_percent_base")
- if "pollmethod" in instance:
- pollmethod = instance["pollmethod"]
- else:
- poll = self.datastore.get(
- fqid_from_collection_and_id(self.model.collection, instance["id"]),
- ["pollmethod"],
- )
- pollmethod = poll.get("pollmethod")
- base_check_onehundred_percent_base(pollmethod, onehundred_percent_base)
-
- def check_state_change(
- self, instance: dict[str, Any], poll: dict[str, Any]
- ) -> bool:
- if poll.get("type") != Poll.TYPE_ANALOG:
- return False
- if poll.get("state") != Poll.STATE_CREATED:
- return False
- check_fields = (
- "votesvalid",
- "votesinvalid",
- "votescast",
- )
- for field in check_fields:
- if instance.get(field):
- return True
- return False
-
- def check_entitled_users_at_stop(
- self, instance: dict[str, Any], poll: PartialModel
- ) -> None:
- field = "entitled_users_at_stop"
- if field not in instance:
- return
- if field not in poll:
- raise ActionException(
- "Can not set 'entitled_users_at_stop' via poll.update"
- )
- if not isinstance(instance[field], list) or any(
- entry
- for entry in instance[field]
- if not isinstance(entry, dict) or not entry.get("user_id")
- ):
- raise ActionException("'entitled_users_at_stop' has the wrong format")
- original_main_user_id_to_entry: dict[int, dict[str, Any]] = {
- date["user_id"]: date for date in poll[field]
- }
- new_main_user_id_to_entry: dict[int, dict[str, Any]] = {
- date["user_id"]: date for date in instance[field]
- }
- original_ids = {user_id for user_id in original_main_user_id_to_entry}
- new_ids = {user_id for user_id in new_main_user_id_to_entry}
- if (len(original_ids) != len(new_ids)) or len(original_ids.difference(new_ids)):
- raise ActionException(
- "Can not change essential 'entitled_users_at_stop' data via poll.update"
- )
- if any(
- original_entry.get(field) != new_main_user_id_to_entry[user_id].get(field)
- for user_id, original_entry in original_main_user_id_to_entry.items()
- for field in ["voted", "present", "user_id", "vote_delegated_to_user_id"]
- ):
- raise ActionException(
- "Can not change essential 'entitled_users_at_stop' data via poll.update"
- )
diff --git a/openslides_backend/action/actions/poll_candidate/__init__.py b/openslides_backend/action/actions/poll_candidate/__init__.py
deleted file mode 100644
index e2b11ce4af..0000000000
--- a/openslides_backend/action/actions/poll_candidate/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import create, delete # noqa
diff --git a/openslides_backend/action/actions/poll_candidate/create.py b/openslides_backend/action/actions/poll_candidate/create.py
deleted file mode 100644
index 933b72cb1f..0000000000
--- a/openslides_backend/action/actions/poll_candidate/create.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from ....models.models import PollCandidate
-from ...generics.create import CreateAction
-from ...util.action_type import ActionType
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-
-
-@register_action("poll_candidate.create", action_type=ActionType.BACKEND_INTERNAL)
-class PollCandidateCreate(CreateAction):
- """
- Internal action to create a poll candiate. It gets the meeting_id from
- its poll candidate list,
- """
-
- model = PollCandidate()
- schema = DefaultSchema(PollCandidate()).get_create_schema(
- required_properties=[
- "user_id",
- "poll_candidate_list_id",
- "weight",
- "meeting_id",
- ]
- )
diff --git a/openslides_backend/action/actions/poll_candidate/delete.py b/openslides_backend/action/actions/poll_candidate/delete.py
deleted file mode 100644
index 8677db3f6d..0000000000
--- a/openslides_backend/action/actions/poll_candidate/delete.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from ....models.models import PollCandidate
-from ...generics.delete import DeleteAction
-from ...util.action_type import ActionType
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-
-
-@register_action("poll_candidate.delete", action_type=ActionType.BACKEND_INTERNAL)
-class PollCandidateDelete(DeleteAction):
- """
- Internal action to delete a poll candidate.
- """
-
- model = PollCandidate()
- schema = DefaultSchema(PollCandidate()).get_delete_schema()
diff --git a/openslides_backend/action/actions/poll_candidate_list/__init__.py b/openslides_backend/action/actions/poll_candidate_list/__init__.py
deleted file mode 100644
index e2b11ce4af..0000000000
--- a/openslides_backend/action/actions/poll_candidate_list/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import create, delete # noqa
diff --git a/openslides_backend/action/actions/poll_candidate_list/create.py b/openslides_backend/action/actions/poll_candidate_list/create.py
deleted file mode 100644
index 5aaa2ebad0..0000000000
--- a/openslides_backend/action/actions/poll_candidate_list/create.py
+++ /dev/null
@@ -1,53 +0,0 @@
-from typing import Any
-
-from ....models.models import PollCandidateList
-from ....shared.schema import required_id_schema
-from ...generics.create import CreateAction
-from ...util.action_type import ActionType
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-from ..poll_candidate.create import PollCandidateCreate
-
-entry_schema = {
- "description": "An entry in a poll_candidate_list create schema",
- "type": "object",
- "properties": {
- "user_id": required_id_schema,
- "weight": {"type": "integer"},
- },
- "additionalProperties": False,
-}
-
-
-@register_action("poll_candidate_list.create", action_type=ActionType.BACKEND_INTERNAL)
-class PollCandidateListCreate(CreateAction):
- """
- Internal action to create a poll_candidate_list.
- """
-
- model = PollCandidateList()
- schema = DefaultSchema(PollCandidateList()).get_create_schema(
- required_properties=["option_id", "meeting_id"],
- additional_required_fields={
- "entries": {"type": "array", "items": entry_schema, "minItems": 1}
- },
- )
-
- def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
- instance = super().update_instance(instance)
- entries = instance.pop("entries")
- self.apply_instance(instance)
- res = self.execute_other_action(
- PollCandidateCreate,
- [
- {
- "user_id": entry["user_id"],
- "weight": entry["weight"],
- "poll_candidate_list_id": instance["id"],
- "meeting_id": instance["meeting_id"],
- }
- for entry in entries
- ],
- )
- instance["poll_candidate_ids"] = [r["id"] for r in res] # type: ignore
- return instance
diff --git a/openslides_backend/action/actions/poll_candidate_list/delete.py b/openslides_backend/action/actions/poll_candidate_list/delete.py
deleted file mode 100644
index 4b87e9c187..0000000000
--- a/openslides_backend/action/actions/poll_candidate_list/delete.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from ....models.models import PollCandidateList
-from ...generics.delete import DeleteAction
-from ...util.action_type import ActionType
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-
-
-@register_action("poll_candidate_list.delete", action_type=ActionType.BACKEND_INTERNAL)
-class PollCandidateListDelete(DeleteAction):
- """
- Internal action to delete a poll candidate.
- """
-
- model = PollCandidateList()
- schema = DefaultSchema(PollCandidateList()).get_delete_schema()
diff --git a/openslides_backend/action/actions/user/merge_mixins.py b/openslides_backend/action/actions/user/merge_mixins.py
index 107393d946..23c216164d 100644
--- a/openslides_backend/action/actions/user/merge_mixins.py
+++ b/openslides_backend/action/actions/user/merge_mixins.py
@@ -4,15 +4,17 @@
from ....models.models import (
AssignmentCandidate,
+ Ballot,
MeetingUser,
MotionEditor,
MotionSubmitter,
MotionSupporter,
MotionWorkingGroupSpeaker,
PersonalNote,
+ Poll,
+ PollConfigOption,
Speaker,
)
-from ....services.database.commands import GetManyRequest
from ....shared.exceptions import ActionException
from ....shared.filters import And, FilterOperator, Or
from ....shared.patterns import Collection, fqid_from_collection_and_id
@@ -113,6 +115,7 @@ def get_full_history_information(self) -> HistoryInformation | None:
class MotionSubmitterMergeMixin(BaseMergeMixin):
+ # ???
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.add_collection_field_groups(
@@ -194,6 +197,36 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
)
+class PollMergeMixin(BaseMergeMixin):
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super().__init__(*args, **kwargs)
+ self.add_collection_field_groups(
+ Poll,
+ {},
+ "meeting_user_id",
+ )
+
+
+class PollConfigOptionMergeMixin(BaseMergeMixin):
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super().__init__(*args, **kwargs)
+ self.add_collection_field_groups(
+ PollConfigOption,
+ {},
+ "meeting_user_id",
+ )
+
+
+class BallotMergeMixin(BaseMergeMixin):
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super().__init__(*args, **kwargs)
+ self.add_collection_field_groups(
+ Ballot,
+ {},
+ "meeting_user_id",
+ )
+
+
class MeetingUserMergeMixin(
PersonalNoteMergeMixin,
MotionWorkingGroupSpeakerMergeMixin,
@@ -201,6 +234,9 @@ class MeetingUserMergeMixin(
MotionSubmitterMergeMixin,
MotionSupporterMergeMixin,
AssignmentCandidateMergeMixin,
+ PollMergeMixin,
+ PollConfigOptionMergeMixin,
+ BallotMergeMixin,
SpeakerMergeMixin,
):
def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -225,6 +261,10 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
"chat_message_ids",
"group_ids",
"structure_level_ids",
+ "poll_voted_ids",
+ "poll_option_ids",
+ "acting_ballot_ids",
+ "represented_ballot_ids",
],
"deep_merge": {
"assignment_candidate_ids": "assignment_candidate",
@@ -275,23 +315,8 @@ def get_merge_comparison_hash(
case _:
return super().get_merge_comparison_hash(collection, model)
- def check_polls_helper(self, meeting_user_ids: list[int]) -> list[str]:
+ def check_polls_helper(self, meeting_users: dict[str, dict[str, Any]]) -> list[str]:
messages: list[str] = []
- meeting_users = self.datastore.get_many(
- [
- GetManyRequest(
- "meeting_user",
- meeting_user_ids,
- [
- "vote_delegations_from_ids",
- "vote_delegated_to_id",
- "meeting_id",
- "group_ids",
- ],
- )
- ]
- ).get("meeting_user", {})
-
group_ids: set[int] = set()
meeting_ids: set[int] = set()
meeting_id_by_group_ids: dict[int, int] = {}
@@ -306,7 +331,7 @@ def check_polls_helper(self, meeting_user_ids: list[int]) -> list[str]:
polls = self.datastore.filter(
"poll",
And(
- FilterOperator("state", "=", "started"),
+ FilterOperator("state", "=", Poll.STATE_STARTED),
Or(
FilterOperator("meeting_id", "=", meeting_id)
for meeting_id in meeting_ids
@@ -359,7 +384,7 @@ def check_polls_helper(self, meeting_user_ids: list[int]) -> list[str]:
if len(
bad_users := [
id_
- for id_ in meeting_user_ids
+ for id_ in list(meeting_users.keys())
if id_ in {*delegator_meeting_user_ids, *proxy_meeting_user_ids}
]
):
diff --git a/openslides_backend/action/actions/user/merge_together.py b/openslides_backend/action/actions/user/merge_together.py
index fe281c134d..6f76a94f85 100644
--- a/openslides_backend/action/actions/user/merge_together.py
+++ b/openslides_backend/action/actions/user/merge_together.py
@@ -1,7 +1,6 @@
from typing import Any, cast
-from psycopg.types.json import Jsonb
-
+from openslides_backend.models.models import Poll
from openslides_backend.services.database.interface import PartialModel
from ....action.mixins.archived_meeting_check_mixin import CheckForArchivedMeetingMixin
@@ -31,7 +30,6 @@
from ..motion_working_group_speaker.update import MotionWorkingGroupSpeakerUpdateAction
from ..personal_note.create import PersonalNoteCreateAction
from ..personal_note.update import PersonalNoteUpdateAction
-from ..poll.update import PollUpdateAction
from ..speaker.create_for_merge import SpeakerCreateForMerge
from ..speaker.delete import SpeakerDeleteAction
from ..speaker.update import SpeakerUpdate
@@ -108,11 +106,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
"committee_management_ids",
"history_entry_ids",
"history_position_ids",
- "option_ids", # throw error if conflict on same poll
- "poll_voted_ids", # throw error if conflict on same poll
- "vote_ids", # throw error if conflict on same poll
- "delegated_vote_ids", # throw error if conflict on same poll
- "poll_candidate_ids", # error if multiple of the users are on the same list, else merge
],
"deep_create_merge": {
"meeting_user_ids": "meeting_user",
@@ -172,43 +165,6 @@ def get_updated_instances(self, action_data: ActionData) -> ActionData:
raise ActionException(
"Users cannot be part of different merges at the same time"
)
- secondary_id_to_main_ids = {
- user_id: instance["id"]
- for instance in action_data
- for user_id in instance.get("user_ids", [])
- }
- polls = self.datastore.filter(
- "poll",
- And(
- FilterOperator("entitled_users_at_stop", "!=", None),
- FilterOperator("entitled_users_at_stop", "!=", Jsonb([])),
- ),
- ["entitled_users_at_stop"],
- )
- poll_payloads: list[dict[str, Any]] = []
- for id_, poll in polls.items():
- entitled: list[dict[str, Any]] = poll["entitled_users_at_stop"]
- changed = False
- for vote in entitled:
- if (
- user_id := (vote.get("user_merged_into_id") or vote.get("user_id"))
- ) in secondary_id_to_main_ids:
- vote["user_merged_into_id"] = secondary_id_to_main_ids[user_id]
- changed = True
- if (
- user_id := (
- vote.get("delegation_user_merged_into_id")
- or vote.get("vote_delegated_to_user_id")
- )
- ) in secondary_id_to_main_ids:
- vote["delegation_user_merged_into_id"] = secondary_id_to_main_ids[
- user_id
- ]
- changed = True
- if changed:
- poll_payloads.append({"id": id_, "entitled_users_at_stop": entitled})
- if len(poll_payloads):
- self.execute_other_action(PollUpdateAction, poll_payloads)
return super().get_updated_instances(action_data)
def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
@@ -406,88 +362,161 @@ def call_other_actions(
def check_polls(self, into: PartialModel, other_models: list[PartialModel]) -> None:
all_models = [into, *other_models]
- vote_poll_ids_per_user_id: dict[int, set[int]] = {
- user["id"]: {poll_id for poll_id in user.get("poll_voted_ids", [])}
- for user in [into, *other_models]
+ # user -> meeting user, same name
+ mu_ids_per_user = {
+ user["id"]: user.get("meeting_user_ids", []) for user in all_models
+ }
+ meeting_user_ids = [
+ mu_id for user in all_models for mu_id in user.get("meeting_user_ids", [])
+ ]
+ poll_conflict_fields = [
+ "poll_voted_ids",
+ "poll_option_ids",
+ "acting_ballot_ids",
+ "represented_ballot_ids",
+ ]
+ helper_fields = [
+ "vote_delegations_from_ids",
+ "vote_delegated_to_id",
+ "meeting_id",
+ "group_ids",
+ ]
+ mu_data = self.datastore.get_many(
+ [
+ GetManyRequest(
+ "meeting_user",
+ meeting_user_ids,
+ ["user_id"] + poll_conflict_fields + helper_fields,
+ )
+ ]
+ )["meeting_user"]
+
+ vote_poll_ids_per_meeting_user_id: dict[int, set[int]] = {
+ meeting_user["id"]: {
+ "user_id": meeting_user["user_id"],
+ "poll_voted_ids": poll_voted_ids,
+ }
+ for id_, meeting_user in mu_data.items()
+ if (poll_voted_ids := meeting_user.get("poll_voted_ids"))
}
+
+ ballot_poll_ids_per_user_id: dict[int, set[int]] = {}
+ for meeting_user in vote_poll_ids_per_meeting_user_id.values():
+ ballot_poll_ids_per_user_id.setdefault(
+ meeting_user["user_id"], set()
+ ).update(set(meeting_user["poll_voted_ids"]))
+ # acting_vote_ids, represented_vote_ids -> mu
+ # meeting_user.poll_option_ids
+ # list: option of config-approval with options
option_poll_ids_per_user_id: dict[int, set[int]] = {}
candidate_list_ids_per_user_id: dict[int, set[int]] = {}
meeting_user_ids: list[int] = []
- for model in all_models:
+ for model in mu_data.values():
if len(
- (pc_ids := model.get("poll_candidate_ids", []))
- + (o_ids := model.get("option_ids", []))
+ # (pc_ids := model.get("poll_candidate_ids", []))
+ # +(o_ids := model.get("poll_option_ids", []))
+ (o_ids := model.get("poll_option_ids", []))
+ (
v_ids := list(
{
id_
for id_ in [
- *model.get("vote_ids", []),
- *model.get("delegated_vote_ids", []),
+ *model.get("acting_ballot_ids", []),
+ *model.get("represented_ballot_ids", []),
]
}
)
)
):
get_many_requests = [
+ # GetManyRequest(
+ # "poll_candidate", pc_ids, ["poll_candidate_list_id"]
+ # ),
GetManyRequest(
- "poll_candidate", pc_ids, ["poll_candidate_list_id"]
- ),
- GetManyRequest(
- "option",
+ "poll_config_option",
o_ids,
- ["poll_id"],
+ ["poll_config_id"],
),
GetManyRequest(
- "vote",
+ "ballot",
v_ids,
- ["option_id"],
+ ["poll_id"],
),
]
many_models = self.datastore.get_many(get_many_requests)
- if pc_ids:
- candidate_list_ids_per_user_id[model["id"]] = {
- poll_candidate["poll_candidate_list_id"]
- for poll_candidate in many_models["poll_candidate"].values()
- if poll_candidate.get("poll_candidate_list_id")
- }
+ # if pc_ids:
+ # candidate_list_ids_per_user_id[model["id"]] = {
+ # poll_candidate["poll_candidate_list_id"]
+ # for poll_candidate in many_models["poll_candidate"].values()
+ # if poll_candidate.get("poll_candidate_list_id")
+ # }
+ # if o_ids:
+ # option_poll_ids_per_user_id[model["id"]] = {
+ # option["poll_id"]
+ # for option in many_models["option"].values()
+ # if option.get("poll_id")
+ # }
if o_ids:
- option_poll_ids_per_user_id[model["id"]] = {
- option["poll_id"]
- for option in many_models["option"].values()
- if option.get("poll_id")
+ option_data = many_models["poll_config_option"]
+ config_ids_per_collection = {}
+ for config in option_data.values():
+ collection, id_ = config["poll_config_id"].split("/")
+ config_ids_per_collection.setdefault(collection, set()).update(
+ id_
+ )
+ gmr = [
+ GetManyRequest(
+ collection,
+ [int(id_) for id_ in ids],
+ ["poll_id"],
+ )
+ for collection, ids in config_ids_per_collection.items()
+ ]
+ r = self.datastore.get_many(gmr)
+ option_poll_ids_per_user_id[model["user_id"]] = {
+ *option_poll_ids_per_user_id.get(model["user_id"], set()),
+ *{
+ cast(int, option["poll_id"])
+ for collection, config in self.datastore.get_many(
+ gmr
+ ).items()
+ for option in config.values()
+ if collection != Poll.CONFIG_TYPE_APPROVAL
+ },
}
- vote_data = many_models["vote"]
- vote_poll_ids_per_user_id[model["id"]] = {
- *vote_poll_ids_per_user_id.get(model["id"], set()),
+ candidate_list_ids_per_user_id[model["user_id"]] = {
+ *candidate_list_ids_per_user_id.get(model["user_id"], set()),
+ *{
+ cast(int, option["poll_id"])
+ for collection, config in self.datastore.get_many(
+ gmr
+ ).items()
+ for option in config.values()
+ if collection == Poll.CONFIG_TYPE_APPROVAL
+ },
+ }
+
+ ballot_data = many_models["ballot"]
+ ballot_poll_ids_per_user_id[model["user_id"]] = {
+ *ballot_poll_ids_per_user_id.get(model["user_id"], set()),
*{
- cast(int, option["poll_id"])
- for option in self.datastore.get_many(
- [
- GetManyRequest(
- "option",
- list(
- {
- id_
- for id_ in [
- vote["option_id"]
- for vote in vote_data.values()
- if vote.get("option_id")
- ]
- }
- ),
- ["poll_id"],
- )
- ]
- )["option"].values()
- if option.get("poll_id")
+ id_
+ for id_ in [
+ ballot["poll_id"] for ballot in ballot_data.values()
+ ]
},
}
- meeting_user_ids += model.get("meeting_user_ids", [])
+ # if v_ids:
+ # vote_poll_ids_per_user_id[model["id"]] = {
+ # vote["poll_id"]
+ # for vote in many_models["vote"].values()
+ # if vote.get("poll_id")
+ # }
+ # meeting_user_ids += model.get("meeting_user_ids", [])
voting_conflicts = {
poll_id
- for id1, poll_ids1 in vote_poll_ids_per_user_id.items()
- for id2, poll_ids2 in vote_poll_ids_per_user_id.items()
+ for id1, poll_ids1 in ballot_poll_ids_per_user_id.items()
+ for id2, poll_ids2 in ballot_poll_ids_per_user_id.items()
for poll_id in poll_ids1.intersection(poll_ids2)
if id1 != id2
}
@@ -505,7 +534,7 @@ def check_polls(self, into: PartialModel, other_models: list[PartialModel]) -> N
for list_id in list_ids1.intersection(list_ids2)
if id1 != id2
}
- messages: list[str] = self.check_polls_helper(meeting_user_ids)
+ messages: list[str] = self.check_polls_helper(mu_data)
if len(voting_conflicts):
messages.append(
f"among the selected users multiple voted in poll(s) {', '.join([str(id_) for id_ in voting_conflicts])}"
@@ -515,24 +544,8 @@ def check_polls(self, into: PartialModel, other_models: list[PartialModel]) -> N
f"multiple of the selected users are among the options in poll(s) {', '.join([str(id_) for id_ in option_conflicts])}"
)
if len(candidate_list_conflicts):
- lists = self.datastore.get_many(
- [
- GetManyRequest(
- "poll_candidate_list",
- list(candidate_list_conflicts),
- ["option_id"],
- )
- ],
- lock_result=False,
- )["poll_candidate_list"]
- option_ids = {c_list["option_id"] for c_list in lists.values()}
- options = self.datastore.get_many(
- [GetManyRequest("option", list(option_ids), ["poll_id"])],
- lock_result=False,
- )["option"]
- poll_ids = {option["poll_id"] for option in options.values()}
messages.append(
- f"multiple of the selected users are in the same candidate list in poll(s) {', '.join([str(id_) for id_ in poll_ids])}"
+ f"multiple of the selected users are in the same candidate list in poll(s) {', '.join([str(id_) for id_ in candidate_list_conflicts])}"
)
if len(messages):
raise ActionException(
diff --git a/openslides_backend/action/actions/user/update.py b/openslides_backend/action/actions/user/update.py
index 0350041301..91359c23dc 100644
--- a/openslides_backend/action/actions/user/update.py
+++ b/openslides_backend/action/actions/user/update.py
@@ -57,11 +57,6 @@ class UserUpdate(
internal_id_fields = [
"is_present_in_meeting_ids",
- "option_ids",
- "poll_candidate_ids",
- "poll_voted_ids",
- "vote_ids",
- "delegated_vote_ids",
"history_entry_ids",
"history_position_ids",
]
diff --git a/openslides_backend/action/actions/vote/__init__.py b/openslides_backend/action/actions/vote/__init__.py
deleted file mode 100644
index 2d608629cc..0000000000
--- a/openslides_backend/action/actions/vote/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import anonymize, create, delete, update # noqa
diff --git a/openslides_backend/action/actions/vote/anonymize.py b/openslides_backend/action/actions/vote/anonymize.py
deleted file mode 100644
index 3a0ea659a8..0000000000
--- a/openslides_backend/action/actions/vote/anonymize.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from typing import Any
-
-from ....models.models import Vote
-from ...generics.update import UpdateAction
-from ...util.action_type import ActionType
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-
-
-@register_action("vote.anonymize", action_type=ActionType.BACKEND_INTERNAL)
-class VoteAnonymize(UpdateAction):
- """
- Action to anonymize a vote by removing the user ids.
- """
-
- model = Vote()
- schema = DefaultSchema(Vote()).get_update_schema()
-
- def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
- instance["user_id"] = None
- instance["delegated_user_id"] = None
- return instance
diff --git a/openslides_backend/action/actions/vote/create.py b/openslides_backend/action/actions/vote/create.py
deleted file mode 100644
index 2846ce44cb..0000000000
--- a/openslides_backend/action/actions/vote/create.py
+++ /dev/null
@@ -1,75 +0,0 @@
-from typing import cast
-
-from openslides_backend.action.util.typing import ActionData
-from openslides_backend.services.database.commands import GetManyRequest
-
-from ....models.models import Vote
-from ...mixins.create_action_with_inferred_meeting import (
- CreateActionWithInferredMeeting,
-)
-from ...util.action_type import ActionType
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-
-
-@register_action("vote.create", action_type=ActionType.BACKEND_INTERNAL)
-class VoteCreate(CreateActionWithInferredMeeting):
- """
- Internal action to create a vote.
- """
-
- model = Vote()
- schema = DefaultSchema(Vote()).get_create_schema(
- required_properties=[
- "weight",
- "value",
- "option_id",
- "user_token",
- ],
- optional_properties=["delegated_user_id", "user_id"],
- )
-
- relation_field_for_meeting = "option_id"
-
- def prefetch(self, action_data: ActionData) -> None:
- self.datastore.get_many(
- [
- GetManyRequest(
- "option",
- list({instance["option_id"] for instance in action_data}),
- ["meeting_id", "vote_ids"],
- ),
- ],
- use_changed_models=False,
- )
- meeting_users = self.datastore.get_many(
- [
- GetManyRequest(
- "meeting_user",
- list(
- {
- cast(int, instance.get(fname))
- for instance in action_data
- for fname in (
- "meeting_user_id",
- "delegated_meeting_user_id",
- )
- if instance.get(fname)
- }
- ),
- ["id", "user_id", "vote_ids", "delegated_vote_ids"],
- ),
- ],
- use_changed_models=False,
- )["meeting_user"]
-
- self.datastore.get_many(
- [
- GetManyRequest(
- "user",
- list({mu["user_id"] for mu in meeting_users.values()}),
- ["id", "poll_voted_ids"],
- ),
- ],
- use_changed_models=False,
- )
diff --git a/openslides_backend/action/actions/vote/delete.py b/openslides_backend/action/actions/vote/delete.py
deleted file mode 100644
index 099e24459c..0000000000
--- a/openslides_backend/action/actions/vote/delete.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from ....models.models import Vote
-from ...generics.delete import DeleteAction
-from ...util.action_type import ActionType
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-
-
-@register_action("vote.delete", action_type=ActionType.BACKEND_INTERNAL)
-class VoteDelete(DeleteAction):
- """
- Action to delete votes.
- """
-
- model = Vote()
- schema = DefaultSchema(Vote()).get_delete_schema()
diff --git a/openslides_backend/action/actions/vote/update.py b/openslides_backend/action/actions/vote/update.py
deleted file mode 100644
index fa34189f1c..0000000000
--- a/openslides_backend/action/actions/vote/update.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from ....models.models import Vote
-from ...generics.update import UpdateAction
-from ...util.action_type import ActionType
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-
-
-@register_action("vote.update", action_type=ActionType.BACKEND_INTERNAL)
-class VoteUpdate(UpdateAction):
- """
- Internal action to update a vote.
- """
-
- model = Vote()
- schema = DefaultSchema(Vote()).get_update_schema(required_properties=["weight"])
diff --git a/openslides_backend/action/actions/vote/user_token_helper.py b/openslides_backend/action/actions/vote/user_token_helper.py
deleted file mode 100644
index e993d0922f..0000000000
--- a/openslides_backend/action/actions/vote/user_token_helper.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from ...util.crypto import get_random_string
-
-
-def get_user_token() -> str:
- return get_random_string(16)
diff --git a/openslides_backend/models/checker.py b/openslides_backend/models/checker.py
index f165599568..ac0abdfdde 100644
--- a/openslides_backend/models/checker.py
+++ b/openslides_backend/models/checker.py
@@ -188,7 +188,17 @@ def check_timestamp(value: Any) -> bool:
collection
for collection, model in model_registry.items()
if model().has_field("meeting_id")
-} | {"meeting", "user", "mediafile"}
+} | {
+ "meeting",
+ "user",
+ "mediafile",
+ "vote",
+ "poll_config_approval",
+ "poll_config_selection",
+ "poll_config_rating_score",
+ "poll_config_rating_approval",
+ "poll_config_option",
+}
class Checker:
@@ -465,6 +475,13 @@ def check_relation(
if collection == "user" and field == "organization_id":
return
+ if (
+ collection == "vote"
+ and field == "poll_id"
+ and str(model["poll_id"]) in self.data.get("poll", {})
+ ):
+ return
+
if isinstance(field_type, RelationField):
foreign_id = model[field]
if not foreign_id:
diff --git a/openslides_backend/models/mixins.py b/openslides_backend/models/mixins.py
index ee78890774..01afc8729e 100644
--- a/openslides_backend/models/mixins.py
+++ b/openslides_backend/models/mixins.py
@@ -78,11 +78,16 @@ class PollModelMixin:
STATE_CREATED = "created"
STATE_STARTED = "started"
STATE_FINISHED = "finished"
- STATE_PUBLISHED = "published"
- TYPE_ANALOG = "analog"
- TYPE_NAMED = "named"
- TYPE_PSEUDOANONYMOUS = "pseudoanonymous"
+ VISIBILITY_MANUALLY = "manually"
+ VISIBILITY_NAMED = "named"
+ VISIBILITY_OPEN = "open"
+ VISIBILITY_SECRET = "secret"
+
+ CONFIG_TYPE_APPROVAL = "poll_config_approval"
+ CONFIG_TYPE_SELECTION = "poll_config_selection"
+ CONFIG_TYPE_RATING_SCORE = "poll_config_rating_score"
+ CONFIG_TYPE_RATING_APPROVAL = "poll_config_rating_approval"
ONEHUNDRED_PERCENT_BASE_Y = "Y"
ONEHUNDRED_PERCENT_BASE_YN = "YN"
@@ -93,3 +98,6 @@ class PollModelMixin:
ONEHUNDRED_PERCENT_BASE_ENTITLED = "entitled"
ONEHUNDRED_PERCENT_BASE_ENTITLED_PRESENT = "entitled_present"
ONEHUNDRED_PERCENT_BASE_DISABLED = "disabled"
+
+ OPTION_TYPE_TEXT = "text"
+ OPTION_TYPE_MEETING_USER = "meeting_user"
diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py
index 419598010c..0923f70dfa 100644
--- a/openslides_backend/models/models.py
+++ b/openslides_backend/models/models.py
@@ -193,6 +193,28 @@ class AssignmentCandidate(Model):
)
+class Ballot(Model):
+ collection = "ballot"
+ verbose_name = "ballot"
+
+ id = fields.IntegerField(required=True, constant=True)
+ weight = fields.DecimalField(constant=True, default="1.000000")
+ split = fields.BooleanField(default=False)
+ value = fields.TextField(constant=True)
+ poll_id = fields.RelationField(
+ to={"poll": "ballot_ids"},
+ required=True,
+ constant=True,
+ equal_fields="meeting_id",
+ )
+ acting_meeting_user_id = fields.RelationField(
+ to={"meeting_user": "acting_ballot_ids"}
+ )
+ represented_meeting_user_id = fields.RelationField(
+ to={"meeting_user": "represented_ballot_ids"}
+ )
+
+
class ChatGroup(Model):
collection = "chat_group"
verbose_name = "chat group"
@@ -926,12 +948,6 @@ class Meeting(Model, MeetingModelMixin):
constraints={"enum": ["first_name", "last_name"]},
)
motion_poll_projection_max_columns = fields.IntegerField(required=True, default=6)
- poll_candidate_list_ids = fields.RelationListField(
- to={"poll_candidate_list": "meeting_id"}, is_view_field=True, is_primary=True
- )
- poll_candidate_ids = fields.RelationListField(
- to={"poll_candidate": "meeting_id"}, is_view_field=True, is_primary=True
- )
meeting_user_ids = fields.RelationListField(
to={"meeting_user": "meeting_id"},
on_delete=fields.OnDelete.CASCADE,
@@ -1034,13 +1050,22 @@ class Meeting(Model, MeetingModelMixin):
poll_default_group_ids = fields.RelationListField(
to={"group": "used_as_poll_default_id"}, is_view_field=True
)
- poll_default_backend = fields.CharField(
- default="fast", constraints={"enum": ["long", "fast"]}
- )
poll_default_live_voting_enabled = fields.BooleanField(
default=False,
constraints={
- "description": "Defines default 'poll.live_voting_enabled' option suggested to user. Is not used in the validations."
+ "description": "Defines default 'poll.published' before finished option suggested to user. Is not used in the validations."
+ },
+ )
+ poll_default_allow_invalid = fields.BooleanField(
+ default=False,
+ constraints={
+ "description": "Defines defaut `poll.allow_invalid` option suggested to user."
+ },
+ )
+ poll_default_allow_vote_split = fields.BooleanField(
+ default=False,
+ constraints={
+ "description": "Defines defaut `poll.allow_vote_split` option suggested to user."
},
)
poll_couple_countdown = fields.BooleanField(default=True)
@@ -1186,18 +1211,6 @@ class Meeting(Model, MeetingModelMixin):
is_view_field=True,
is_primary=True,
)
- option_ids = fields.RelationListField(
- to={"option": "meeting_id"},
- on_delete=fields.OnDelete.CASCADE,
- is_view_field=True,
- is_primary=True,
- )
- vote_ids = fields.RelationListField(
- to={"vote": "meeting_id"},
- on_delete=fields.OnDelete.CASCADE,
- is_view_field=True,
- is_primary=True,
- )
assignment_ids = fields.RelationListField(
to={"assignment": "meeting_id"},
on_delete=fields.OnDelete.CASCADE,
@@ -1588,6 +1601,28 @@ class MeetingUser(Model):
is_view_field=True,
equal_fields="meeting_id",
)
+ poll_voted_ids = fields.RelationListField(
+ to={"poll": "voted_ids"},
+ is_view_field=True,
+ is_primary=True,
+ write_fields=(
+ "nm_meeting_user_poll_voted_ids_poll_t",
+ "meeting_user_id",
+ "poll_id",
+ [],
+ ),
+ )
+ poll_option_ids = fields.RelationListField(
+ to={"poll_config_option": "meeting_user_id"},
+ is_view_field=True,
+ is_primary=True,
+ )
+ acting_ballot_ids = fields.RelationListField(
+ to={"ballot": "acting_meeting_user_id"}, is_view_field=True
+ )
+ represented_ballot_ids = fields.RelationListField(
+ to={"ballot": "represented_meeting_user_id"}, is_view_field=True
+ )
chat_message_ids = fields.RelationListField(
to={"chat_message": "meeting_user_id"},
is_view_field=True,
@@ -1801,13 +1836,6 @@ class Motion(Model):
is_view_field=True,
equal_fields="meeting_id",
)
- option_ids = fields.RelationListField(
- to={"option": "content_object_id"},
- on_delete=fields.OnDelete.CASCADE,
- is_view_field=True,
- is_primary=True,
- equal_fields="meeting_id",
- )
change_recommendation_ids = fields.RelationListField(
to={"motion_change_recommendation": "motion_id"},
on_delete=fields.OnDelete.CASCADE,
@@ -2256,43 +2284,6 @@ class MotionWorkingGroupSpeaker(Model):
)
-class Option(Model):
- collection = "option"
- verbose_name = "option"
-
- id = fields.IntegerField(required=True, constant=True)
- weight = fields.IntegerField(default=10000)
- text = fields.HTMLStrictField()
- yes = fields.DecimalField()
- no = fields.DecimalField()
- abstain = fields.DecimalField()
- poll_id = fields.RelationField(
- to={"poll": "option_ids"}, constant=True, equal_fields="meeting_id"
- )
- used_as_global_option_in_poll_id = fields.RelationField(
- to={"poll": "global_option_id"}, constant=True, equal_fields="meeting_id"
- )
- vote_ids = fields.RelationListField(
- to={"vote": "option_id"},
- on_delete=fields.OnDelete.CASCADE,
- is_view_field=True,
- is_primary=True,
- equal_fields="meeting_id",
- )
- content_object_id = fields.GenericRelationField(
- to={
- "poll_candidate_list": "option_id",
- "user": "option_ids",
- "motion": "option_ids",
- },
- constant=True,
- equal_fields="meeting_id",
- )
- meeting_id = fields.RelationField(
- to={"meeting": "option_ids"}, required=True, constant=True
- )
-
-
class Organization(Model):
collection = "organization"
verbose_name = "organization"
@@ -2445,55 +2436,40 @@ class Poll(Model, PollModelMixin):
id = fields.IntegerField(required=True, constant=True)
title = fields.CharField(required=True)
- description = fields.CharField()
- type = fields.CharField(
+ config_id = fields.GenericRelationField(
+ to={
+ "poll_config_stv_scottish": "poll_id",
+ "poll_config_rating_approval": "poll_id",
+ "poll_config_rating_score": "poll_id",
+ "poll_config_selection": "poll_id",
+ "poll_config_approval": "poll_id",
+ },
required=True,
- constraints={"enum": ["analog", "named", "pseudoanonymous", "cryptographic"]},
)
- backend = fields.CharField(
- required=True, default="fast", constraints={"enum": ["long", "fast"]}
- )
- is_pseudoanonymized = fields.BooleanField()
- pollmethod = fields.CharField(
- required=True, constraints={"enum": ["Y", "YN", "YNA", "N"]}
+ visibility = fields.CharField(
+ required=True, constraints={"enum": ["manually", "named", "open", "secret"]}
)
state = fields.CharField(
- default="created",
- constraints={"enum": ["created", "started", "finished", "published"]},
- )
- min_votes_amount = fields.IntegerField(default=1, constraints={"minimum": 1})
- max_votes_amount = fields.IntegerField(default=1, constraints={"minimum": 1})
- max_votes_per_option = fields.IntegerField(default=1, constraints={"minimum": 1})
- global_yes = fields.BooleanField(default=False)
- global_no = fields.BooleanField(default=False)
- global_abstain = fields.BooleanField(default=False)
- onehundred_percent_base = fields.CharField(
- required=True,
- default="disabled",
+ default="created", constraints={"enum": ["created", "started", "finished"]}
+ )
+ result = fields.TextField(
constraints={
- "enum": [
- "Y",
- "YN",
- "YNA",
- "N",
- "valid",
- "cast",
- "entitled",
- "entitled_present",
- "disabled",
- ]
- },
+ "description": "Calculated result. The format depends on the value in poll/method. Can be manually set when visibility is set to manually."
+ }
)
- votesvalid = fields.DecimalField()
- votesinvalid = fields.DecimalField()
- votescast = fields.DecimalField()
- entitled_users_at_stop = fields.JSONField()
- live_voting_enabled = fields.BooleanField(
+ published = fields.BooleanField(
+ default=False, constraints={"description": "If true, users can see the result."}
+ )
+ allow_invalid = fields.BooleanField(
default=False,
constraints={
- "description": "If true, the vote service sends the votes of the users to the autoupdate service."
+ "description": "If true, the vote service does not validate. This is always the case for secret polls."
},
)
+ allow_vote_split = fields.BooleanField(
+ default=False,
+ constraints={"description": "If true, users can split there vote."},
+ )
sequential_number = fields.IntegerField(
required=True,
read_only=True,
@@ -2509,24 +2485,22 @@ class Poll(Model, PollModelMixin):
constant=True,
equal_fields="meeting_id",
)
- option_ids = fields.RelationListField(
- to={"option": "poll_id"},
+ ballot_ids = fields.RelationListField(
+ to={"ballot": "poll_id"},
on_delete=fields.OnDelete.CASCADE,
is_view_field=True,
is_primary=True,
equal_fields="meeting_id",
)
- global_option_id = fields.RelationField(
- to={"option": "used_as_global_option_in_poll_id"},
- on_delete=fields.OnDelete.CASCADE,
- constant=True,
- equal_fields="meeting_id",
- )
voted_ids = fields.RelationListField(
- to={"user": "poll_voted_ids"},
+ to={"meeting_user": "poll_voted_ids"},
is_view_field=True,
- is_primary=True,
- write_fields=("nm_poll_voted_ids_user_t", "poll_id", "user_id", []),
+ write_fields=(
+ "nm_meeting_user_poll_voted_ids_poll_t",
+ "poll_id",
+ "meeting_user_id",
+ [],
+ ),
)
entitled_group_ids = fields.RelationListField(
to={"group": "poll_ids"},
@@ -2545,46 +2519,102 @@ class Poll(Model, PollModelMixin):
)
-class PollCandidate(Model):
- collection = "poll_candidate"
- verbose_name = "poll candidate"
+class PollConfigApproval(Model):
+ collection = "poll_config_approval"
+ verbose_name = "poll config approval"
id = fields.IntegerField(required=True, constant=True)
- poll_candidate_list_id = fields.RelationField(
- to={"poll_candidate_list": "poll_candidate_ids"},
+ poll_id = fields.RelationField(to={"poll": "config_id"}, required=True)
+ option_ids = fields.RelationListField(
+ to={"poll_config_option": "poll_config_id"},
+ on_delete=fields.OnDelete.CASCADE,
+ is_view_field=True,
+ )
+ allow_abstain = fields.BooleanField(default=True)
+
+
+class PollConfigOption(Model):
+ collection = "poll_config_option"
+ verbose_name = "poll config option"
+
+ id = fields.IntegerField(required=True, constant=True)
+ poll_config_id = fields.GenericRelationField(
+ to={
+ "poll_config_stv_scottish": "option_ids",
+ "poll_config_rating_approval": "option_ids",
+ "poll_config_rating_score": "option_ids",
+ "poll_config_selection": "option_ids",
+ "poll_config_approval": "option_ids",
+ },
required=True,
- constant=True,
- equal_fields="meeting_id",
)
- user_id = fields.RelationField(to={"user": "poll_candidate_ids"}, constant=True)
- weight = fields.IntegerField(required=True)
- meeting_id = fields.RelationField(
- to={"meeting": "poll_candidate_ids"}, required=True, constant=True
+ weight = fields.IntegerField()
+ text = fields.CharField()
+ meeting_user_id = fields.RelationField(to={"meeting_user": "poll_option_ids"})
+
+
+class PollConfigRatingApproval(Model):
+ collection = "poll_config_rating_approval"
+ verbose_name = "poll config rating approval"
+
+ id = fields.IntegerField(required=True, constant=True)
+ poll_id = fields.RelationField(to={"poll": "config_id"}, required=True)
+ option_ids = fields.RelationListField(
+ to={"poll_config_option": "poll_config_id"},
+ on_delete=fields.OnDelete.CASCADE,
+ is_view_field=True,
)
+ max_options_amount = fields.IntegerField(default=0)
+ min_options_amount = fields.IntegerField(default=0)
+ allow_abstain = fields.BooleanField(default=True)
-class PollCandidateList(Model):
- collection = "poll_candidate_list"
- verbose_name = "poll candidate list"
+class PollConfigRatingScore(Model):
+ collection = "poll_config_rating_score"
+ verbose_name = "poll config rating score"
id = fields.IntegerField(required=True, constant=True)
- poll_candidate_ids = fields.RelationListField(
- to={"poll_candidate": "poll_candidate_list_id"},
+ poll_id = fields.RelationField(to={"poll": "config_id"}, required=True)
+ option_ids = fields.RelationListField(
+ to={"poll_config_option": "poll_config_id"},
on_delete=fields.OnDelete.CASCADE,
is_view_field=True,
- is_primary=True,
- equal_fields="meeting_id",
)
- meeting_id = fields.RelationField(
- to={"meeting": "poll_candidate_list_ids"}, required=True, constant=True
+ max_options_amount = fields.IntegerField(default=0)
+ min_options_amount = fields.IntegerField(default=0)
+ max_votes_per_option = fields.IntegerField(default=0)
+ max_vote_sum = fields.IntegerField(default=0)
+ min_vote_sum = fields.IntegerField(default=0)
+
+
+class PollConfigSelection(Model):
+ collection = "poll_config_selection"
+ verbose_name = "poll config selection"
+
+ id = fields.IntegerField(required=True, constant=True)
+ poll_id = fields.RelationField(to={"poll": "config_id"}, required=True)
+ option_ids = fields.RelationListField(
+ to={"poll_config_option": "poll_config_id"},
+ on_delete=fields.OnDelete.CASCADE,
+ is_view_field=True,
)
- option_id = fields.RelationField(
- to={"option": "content_object_id"},
+ max_options_amount = fields.IntegerField(default=0)
+ min_options_amount = fields.IntegerField(default=0)
+ allow_nota = fields.BooleanField(default=False)
+
+
+class PollConfigStvScottish(Model):
+ collection = "poll_config_stv_scottish"
+ verbose_name = "poll config stv scottish"
+
+ id = fields.IntegerField(required=True, constant=True)
+ poll_id = fields.RelationField(to={"poll": "config_id"}, required=True)
+ option_ids = fields.RelationListField(
+ to={"poll_config_option": "poll_config_id"},
+ on_delete=fields.OnDelete.CASCADE,
is_view_field=True,
- required=True,
- constant=True,
- equal_fields="meeting_id",
)
+ posts = fields.IntegerField(default=1, constraints={"minimum": 1})
class Projection(Model):
@@ -3111,23 +3141,6 @@ class User(Model):
on_delete=fields.OnDelete.CASCADE,
is_view_field=True,
)
- poll_voted_ids = fields.RelationListField(
- to={"poll": "voted_ids"},
- is_view_field=True,
- write_fields=("nm_poll_voted_ids_user_t", "user_id", "poll_id", []),
- )
- option_ids = fields.RelationListField(
- to={"option": "content_object_id"}, is_view_field=True, is_primary=True
- )
- vote_ids = fields.RelationListField(
- to={"vote": "user_id"}, is_view_field=True, is_primary=True
- )
- delegated_vote_ids = fields.RelationListField(
- to={"vote": "delegated_user_id"}, is_view_field=True, is_primary=True
- )
- poll_candidate_ids = fields.RelationListField(
- to={"poll_candidate": "user_id"}, is_view_field=True, is_primary=True
- )
home_committee_id = fields.RelationField(to={"committee": "native_user_ids"})
history_position_ids = fields.RelationListField(
to={"history_position": "user_id"}, is_view_field=True
@@ -3146,24 +3159,3 @@ class User(Model):
organization_id = fields.OrganizationField(
to={"organization": "user_ids"}, required=True, constant=True
)
-
-
-class Vote(Model):
- collection = "vote"
- verbose_name = "vote"
-
- id = fields.IntegerField(required=True, constant=True)
- weight = fields.DecimalField(constant=True)
- value = fields.CharField(constant=True)
- user_token = fields.CharField(required=True, constant=True)
- option_id = fields.RelationField(
- to={"option": "vote_ids"},
- required=True,
- constant=True,
- equal_fields="meeting_id",
- )
- user_id = fields.RelationField(to={"user": "vote_ids"})
- delegated_user_id = fields.RelationField(to={"user": "delegated_vote_ids"})
- meeting_id = fields.RelationField(
- to={"meeting": "vote_ids"}, required=True, constant=True
- )
diff --git a/openslides_backend/services/vote/adapter.py b/openslides_backend/services/vote/adapter.py
index 67a000bbc8..740a1cfdc9 100644
--- a/openslides_backend/services/vote/adapter.py
+++ b/openslides_backend/services/vote/adapter.py
@@ -1,4 +1,4 @@
-from typing import Any
+from typing import Any, Literal
import requests
import simplejson as json
@@ -54,21 +54,35 @@ def make_request(self, endpoint: str, payload: dict[str, Any] | None = None) ->
)
raise VoteServiceException(f"Cannot reach the vote service on {endpoint}.")
- def start(self, id: int) -> None:
- endpoint = self.get_endpoint("start", id)
- self.retrieve(endpoint)
+ def create(self, payload: dict[str, Any]) -> dict[str, Any]:
+ endpoint = self.get_endpoint("create")
+ return self.retrieve(endpoint, payload)
+
+ def update(self, id: int, payload: dict[str, Any]) -> dict[str, Any]:
+ endpoint = self.get_endpoint("update", id)
+ return self.retrieve(endpoint, payload)
- def stop(self, id: int) -> dict[str, Any]:
- endpoint = self.get_endpoint("stop", id)
+ def delete(self, id: int) -> dict[str, Any]:
+ endpoint = self.get_endpoint("delete", id)
return self.retrieve(endpoint)
- def clear(self, id: int) -> None:
- endpoint = self.get_endpoint("clear", id)
- self.retrieve(endpoint)
+ def start(self, id: int) -> dict[str, Any]:
+ endpoint = self.get_endpoint("start", id)
+ return self.retrieve(endpoint)
- def clear_all(self) -> None:
- endpoint = self.get_endpoint("clear_all")
- self.retrieve(endpoint)
+ def finalize(
+ self,
+ id: int,
+ optional_attributes: list[Literal["publish", "anonymize"]] = [],
+ ) -> dict[str, Any]:
+ endpoint = self.get_endpoint("finalize", id)
+ if optional_attributes:
+ endpoint += f"&{'&'.join(optional_attributes)}"
+ return self.retrieve(endpoint)
+
+ def reset(self, id: int) -> dict[str, Any]:
+ endpoint = self.get_endpoint("reset", id)
+ return self.retrieve(endpoint)
def get_endpoint(self, route: str, id: int | None = None) -> str:
return f"{self.url}/{route}" + (f"?id={id}" if id else "")
diff --git a/openslides_backend/services/vote/interface.py b/openslides_backend/services/vote/interface.py
index 7c22cb3af4..7a468a62f0 100644
--- a/openslides_backend/services/vote/interface.py
+++ b/openslides_backend/services/vote/interface.py
@@ -1,5 +1,5 @@
from abc import abstractmethod
-from typing import Any, Protocol
+from typing import Any, Literal, Protocol
from ..shared.authenticated_service import AuthenticatedServiceInterface
@@ -10,14 +10,21 @@ class VoteService(AuthenticatedServiceInterface, Protocol):
"""
@abstractmethod
- def start(self, id: int) -> None: ...
+ def create(self, payload: dict[str, Any]) -> dict[str, Any]: ...
@abstractmethod
- def stop(self, id: int) -> dict[str, Any]: ...
+ def update(self, id: int, payload: dict[str, Any]) -> dict[str, Any]: ...
@abstractmethod
- def clear(self, id: int) -> None: ...
+ def delete(self, id: int) -> dict[str, Any]: ...
@abstractmethod
- def clear_all(self) -> None:
- """Only for testing purposes."""
+ def start(self, id: int) -> dict[str, Any]: ...
+
+ @abstractmethod
+ def finalize(
+ self, id: int, optional_attributes: list[Literal["publish", "anonymize"]] = []
+ ) -> dict[str, Any]: ...
+
+ @abstractmethod
+ def reset(self, id: int) -> dict[str, Any]: ...
diff --git a/openslides_backend/shared/env.py b/openslides_backend/shared/env.py
index 01dca41d14..f0098c149d 100644
--- a/openslides_backend/shared/env.py
+++ b/openslides_backend/shared/env.py
@@ -45,7 +45,7 @@ class Environment(Env):
"OPENTELEMETRY_ENABLED": "false",
"PRESENTER_PORT": "9003",
"VOTE_HOST": "vote",
- "VOTE_PATH": "/internal/vote",
+ "VOTE_PATH": "/system/vote",
"VOTE_PORT": "9013",
"VOTE_PROTOCOL": "http",
"DATABASE_HOST": "",
diff --git a/openslides_backend/shared/export_helper.py b/openslides_backend/shared/export_helper.py
index 5d8b95db10..f9a0940619 100644
--- a/openslides_backend/shared/export_helper.py
+++ b/openslides_backend/shared/export_helper.py
@@ -15,15 +15,13 @@
RelationField,
RelationListField,
)
-from ..models.models import Meeting
+from ..models.models import Ballot, Meeting
from ..services.database.commands import GetManyRequest
from ..services.database.interface import Database
from .patterns import collection_from_fqid, fqid_from_collection_and_id, id_from_fqid
FORBIDDEN_FIELDS = ["forwarded_motion_ids"]
-NON_CASCADING_MEETING_RELATION_LISTS = ["poll_candidate_list_ids", "poll_candidate_ids"]
-
HISTORY_FIELDS_PER_COLLECTION = {
"meeting": ["relevant_history_entry_ids"],
"user": ["history_entry_ids", "history_position_ids"],
@@ -208,6 +206,59 @@ def export_meeting(
user_ids.add(results["meeting_user"][id_]["user_id"])
add_users(list(user_ids), export, meeting_id, datastore, internal_target)
+ # Get related ballots
+ if polls := export.get("poll"):
+ ballot_ids = [
+ ballot_id
+ for poll in polls.values()
+ for ballot_id in poll.get("ballot_ids", [])
+ ]
+ if ballot_ids:
+ ballots = datastore.get_many(
+ [GetManyRequest("ballot", ballot_ids, get_fields_for_export("ballot"))],
+ lock_result=False,
+ use_changed_models=False,
+ )["ballot"]
+ for ballot_id, instance in ballots.items():
+ export.setdefault("ballot", {})[str(ballot_id)] = instance
+ for field_name, value in instance.items():
+ model_field = Ballot().get_field(field_name)
+ if (
+ not isinstance(model_field, RelationField)
+ or model_field.get_own_field_name() == "poll_id"
+ ):
+ continue
+ collection, relation_field = next(iter(model_field.to.items()))
+ export[collection][str(value)].setdefault(
+ relation_field, []
+ ).append(ballot_id)
+
+ config_ids = [poll.get("config_id") for poll in polls.values()]
+
+ for config_fqid in config_ids:
+ collection, id_ = config_fqid.split("/")
+ instance = datastore.get(
+ config_fqid,
+ list(get_fields_for_export(collection)),
+ lock_result=False,
+ use_changed_models=False,
+ )
+ export.setdefault(collection, {})[str(id_)] = instance
+ if option_ids := instance.get("option_ids"):
+ options = datastore.get_many(
+ [
+ GetManyRequest(
+ "poll_config_option",
+ option_ids,
+ get_fields_for_export("poll_config_option"),
+ )
+ ],
+ lock_result=False,
+ use_changed_models=False,
+ )["poll_config_option"]
+ for id_, option in options.items():
+ export.setdefault("poll_config_option", {})[str(id_)] = option
+
# Sort instances by id within each collection
for collection, instances in export.items():
if collection == "_migration_index":
@@ -284,15 +335,9 @@ def add_users(
gender_dict = datastore.get_all("gender", ["name"], lock_result=False)
user["gender"] = gender_dict.get(gender_id, {}).get("name")
# limit user fields to exported objects
- collection_field_tupels = [
- ("meeting_user", "meeting_user_ids"),
- ("poll", "poll_voted_ids"),
- ("option", "option_ids"),
- ("vote", "vote_ids"),
- ("poll_candidate", "poll_candidate_ids"),
- ("vote", "delegated_vote_ids"),
- ]
- for collection, fname in collection_field_tupels:
+ # Check voting related fields for mu
+ collection_field_tuples = [("meeting_user", "meeting_user_ids")]
+ for collection, fname in collection_field_tuples:
user[fname] = [
id_
for id_ in user.get(fname, [])
@@ -326,11 +371,8 @@ def get_relation_fields() -> Iterable[RelationListField]:
isinstance(field, RelationListField)
and field not in HISTORY_FIELDS_PER_COLLECTION["meeting"]
and (
- (
- field.on_delete == OnDelete.CASCADE
- and field.get_own_field_name().endswith("_ids")
- )
- or field.get_own_field_name() in NON_CASCADING_MEETING_RELATION_LISTS
+ field.on_delete == OnDelete.CASCADE
+ and field.get_own_field_name().endswith("_ids")
)
):
yield field
diff --git a/tests/database/reader/system/util.py b/tests/database/reader/system/util.py
index 853e6293ae..b6ada342a2 100644
--- a/tests/database/reader/system/util.py
+++ b/tests/database/reader/system/util.py
@@ -77,13 +77,12 @@
"meeting_ids": None,
"is_present_in_meeting_ids": None,
"meeting_user_ids": None,
- "option_ids": None,
- "poll_candidate_ids": None,
- "poll_voted_ids": None,
- "vote_ids": None,
+ # "poll_candidate_ids": None,
+ # "poll_voted_ids": None,
+ # "acting_vote_ids": None,
"committee_ids": None,
"committee_management_ids": None,
- "delegated_vote_ids": None,
+ # "represented_vote_ids": None,
"organization_id": 1,
},
2: {
@@ -114,13 +113,12 @@
"meeting_ids": None,
"is_present_in_meeting_ids": None,
"meeting_user_ids": None,
- "option_ids": None,
- "poll_candidate_ids": None,
- "poll_voted_ids": None,
- "vote_ids": None,
+ # "poll_candidate_ids": None,
+ # "poll_voted_ids": None,
+ # "acting_vote_ids": None,
"committee_ids": None,
"committee_management_ids": None,
- "delegated_vote_ids": None,
+ # "represented_vote_ids": None,
"organization_id": 1,
},
3: {
@@ -151,13 +149,12 @@
"meeting_ids": None,
"is_present_in_meeting_ids": None,
"meeting_user_ids": None,
- "option_ids": None,
- "poll_candidate_ids": None,
- "poll_voted_ids": None,
- "vote_ids": None,
+ # "poll_candidate_ids": None,
+ # "poll_voted_ids": None,
+ # "acting_vote_ids": None,
"committee_ids": None,
"committee_management_ids": None,
- "delegated_vote_ids": None,
+ # "represented_vote_ids": None,
"organization_id": 1,
},
},
diff --git a/tests/system/action/agenda_item/test_forward.py b/tests/system/action/agenda_item/test_forward.py
index 9f54654748..039b08ad4d 100644
--- a/tests/system/action/agenda_item/test_forward.py
+++ b/tests/system/action/agenda_item/test_forward.py
@@ -5,6 +5,7 @@
from psycopg.types.json import Jsonb
from openslides_backend.action.actions.speaker.speech_state import SpeechState
+from openslides_backend.models.models import Poll
from openslides_backend.shared.util import ONE_ORGANIZATION_FQID, ONE_ORGANIZATION_ID
from tests.system.action.base import BaseActionTestCase
@@ -3291,19 +3292,25 @@ def test_forward_structure_level_transfer_only_via_meeting_users(self) -> None:
"poll/1234": {
"meeting_id": 1,
"title": "Will not transfer",
- "type": "pseudoanonymous",
- "backend": "fast",
- "pollmethod": "Y",
- "state": "created",
+ "visibility": Poll.VISIBILITY_SECRET,
+ "config_id": "poll_config_rating_approval/34",
+ "state": Poll.STATE_CREATED,
"min_votes_amount": 1,
"max_votes_amount": 1,
"max_votes_per_option": 1,
- "onehundred_percent_base": "Y",
- "sequential_number": 1,
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_Y,
"content_object_id": "topic/11",
},
- "option/123": {"meeting_id": 1, "poll_id": 1234, "text": "Option A"},
- "option/234": {"meeting_id": 1, "poll_id": 1234, "text": "Option B"},
+ "poll_config_approval/34": {"poll_id": 1, "allow_abstain": False},
+ "poll_config_option/123": {
+ "meeting_id": 1,
+ "poll_id": 1234,
+ "text": "Option A",
+ },
+ "poll_config_option/234": {
+ "poll_config_id": "poll_config_approval/34",
+ "text": "Option B",
+ },
# mediafiles
"mediafile/3": {
"create_timestamp": datetime.fromtimestamp(300, ZoneInfo("UTC")),
@@ -3480,7 +3487,8 @@ def test_forward_structure_level_transfer_only_via_meeting_users(self) -> None:
"mediafile": 4,
"meeting_mediafile": 36,
"poll": 1235,
- "option": 235,
+ "poll_config_approval": 35,
+ "poll_config_option": 235,
"speaker": 38,
"structure_level_list_of_speakers": 15,
"point_of_order_category": 8,
diff --git a/tests/system/action/meeting/test_archive.py b/tests/system/action/meeting/test_archive.py
index 6421b852a1..31939399c2 100644
--- a/tests/system/action/meeting/test_archive.py
+++ b/tests/system/action/meeting/test_archive.py
@@ -102,19 +102,21 @@ def test_archive_meeting_with_inactive_speakers(self) -> None:
response = self.request("meeting.archive", {"id": 1})
self.assert_status_code(response, 200)
- def create_poll(self, base: int, state: str) -> None:
+ def create_poll(self, base: int, state: str, published: bool = False) -> None:
self.set_models(
{
f"poll/{base}": {
"title": f"Poll {base}",
- "type": Poll.TYPE_NAMED,
- "backend": "fast",
- "pollmethod": "YNA",
- "onehundred_percent_base": "YNA",
+ "config_id": f"poll_config_approval/{base}",
+ "visibility": Poll.VISIBILITY_NAMED,
"meeting_id": 1,
"content_object_id": "motion/1",
"state": state,
- }
+ "published": published,
+ },
+ f"poll_config_approval/{base}": {
+ "poll_id": base,
+ },
}
)
@@ -122,7 +124,7 @@ def test_archive_meeting_with_inactive_polls(self) -> None:
self.create_motion(1)
self.create_poll(1, Poll.STATE_CREATED)
self.create_poll(2, Poll.STATE_FINISHED)
- self.create_poll(3, Poll.STATE_PUBLISHED)
+ self.create_poll(3, Poll.STATE_FINISHED, True)
response = self.request("meeting.archive", {"id": 1})
self.assert_status_code(response, 200)
diff --git a/tests/system/action/meeting/test_clone.py b/tests/system/action/meeting/test_clone.py
index b12f410171..e0c43d17f2 100644
--- a/tests/system/action/meeting/test_clone.py
+++ b/tests/system/action/meeting/test_clone.py
@@ -9,7 +9,8 @@
from openslides_backend.action.action_worker import ActionWorkerState
from openslides_backend.migrations.migration_helper import MigrationHelper
from openslides_backend.models.checker import Checker, CheckException
-from openslides_backend.models.models import AgendaItem, Meeting
+from openslides_backend.models.mixins import MeetingModelMixin
+from openslides_backend.models.models import AgendaItem, Meeting, Poll
from openslides_backend.permissions.management_levels import OrganizationManagementLevel
from openslides_backend.permissions.permissions import Permissions
from openslides_backend.shared.export_helper import get_fields_for_export
@@ -652,16 +653,6 @@ def test_clone_with_personal_note(self) -> None:
"personal_note/2", {"meeting_user_id": 2, "meeting_id": 2}
)
- def test_clone_with_option(self) -> None:
- self.set_test_data_with_admin()
- self.set_models({"option/1": {"content_object_id": "user/1", "meeting_id": 1}})
- response = self.request("meeting.clone", {"meeting_id": 1})
- self.assert_status_code(response, 200)
- self.assert_model_exists("user/1", {"option_ids": [1, 2]})
- self.assert_model_exists(
- "option/2", {"content_object_id": "user/1", "meeting_id": 2}
- )
-
def test_clone_with_mediafile(self) -> None:
self.set_test_data_with_admin()
self.set_models(
@@ -1657,41 +1648,46 @@ def test_clone_vote_delegation(self) -> None:
)
def test_clone_vote_delegated_vote(self) -> None:
- self.set_test_data_with_admin()
+ self.set_test_data()
self.create_meeting(4)
+ self.create_motion(1, 1)
+ self.create_motion(4, 4)
self.set_user_groups(1, [2, 5])
- self.set_models(
- {
- "vote/1": {
- "user_id": 1,
- "delegated_user_id": 1,
- "meeting_id": 1,
- "option_id": 1,
- "user_token": "asdfgh",
+ for poll_id in range(1, 3):
+ self.set_models(
+ {
+ f"poll/{poll_id}": {
+ "title": f"Poll {poll_id}",
+ "meeting_id": 1 if poll_id == 1 else 4,
+ "content_object_id": f"motion/{poll_id}",
+ "visibility": Poll.VISIBILITY_NAMED,
+ "config_id": f"poll_config_rating_approval/{poll_id}",
+ "state": Poll.STATE_STARTED,
+ },
+ f"poll_config_rating_approval/{poll_id}": {"poll_id": poll_id},
+ f"ballot/{poll_id}": {
+ "acting_meeting_user_id": poll_id,
+ "represented_meeting_user_id": poll_id,
+ "poll_id": poll_id,
+ },
},
- "vote/2": {
- "user_id": 1,
- "delegated_user_id": 1,
- "meeting_id": 4,
- "option_id": 2,
- "user_token": "hjkl",
- },
- "option/1": {"meeting_id": 1},
- "option/2": {"meeting_id": 4},
- },
- )
+ )
response = self.request("meeting.clone", {"meeting_id": 1})
self.assert_status_code(response, 200)
self.assert_model_exists(
- "vote/3",
- {"user_id": 1, "delegated_user_id": 1, "option_id": 3, "meeting_id": 5},
+ "ballot/3",
+ {
+ "acting_meeting_user_id": 1,
+ "represented_meeting_user_id": 1,
+ "poll_id": 3,
+ },
)
self.assert_model_exists(
"user/1",
{
"meeting_user_ids": [1, 2, 3],
- "vote_ids": [1, 2, 3],
- "delegated_vote_ids": [1, 2, 3],
+ # "acting_vote_ids": [1, 2, 3, 4],
+ # "represented_vote_ids": [1, 2, 3, 4],
"meeting_ids": [1, 4, 5],
},
)
@@ -1908,74 +1904,69 @@ def test_clone_with_list_election(self) -> None:
"open_posts": 0,
"list_of_speakers_id": 1,
},
- "poll_candidate/1": {
- "id": 1,
- "weight": 1,
- "user_id": 2,
- "meeting_id": 1,
- "poll_candidate_list_id": 1,
- },
- "poll_candidate/2": {
- "id": 2,
- "weight": 2,
- "user_id": 3,
- "meeting_id": 1,
- "poll_candidate_list_id": 1,
- },
- "poll_candidate/3": {
- "id": 3,
- "weight": 3,
- "user_id": 4,
- "meeting_id": 1,
- "poll_candidate_list_id": 1,
- },
- "poll_candidate_list/1": {
- "id": 1,
- "option_id": 1,
- "meeting_id": 1,
- "poll_candidate_ids": [1, 2, 3],
- },
- "option/1": {
- "id": 1,
- "weight": 1,
- "poll_id": 1,
- "meeting_id": 1,
- "content_object_id": "poll_candidate_list/1",
- },
- "option/2": {
- "id": 2,
- "text": "global option",
- "weight": 1,
- "meeting_id": 1,
- "used_as_global_option_in_poll_id": 1,
- },
"poll/1": {
"id": 1,
- "type": "pseudoanonymous",
- "state": "created",
"title": "First election",
- "backend": "fast",
- "global_no": False,
- "votescast": "0.000000",
- "global_yes": False,
"meeting_id": 1,
- "option_ids": [1],
- "pollmethod": "YNA",
- "votesvalid": "0.000000",
- "votesinvalid": "0.000000",
- "global_abstain": False,
- "global_option_id": 2,
- "max_votes_amount": 1,
- "min_votes_amount": 1,
"content_object_id": "assignment/1",
- "is_pseudoanonymized": True,
- "max_votes_per_option": 1,
- "onehundred_percent_base": "disabled",
+ "visibility": Poll.VISIBILITY_SECRET,
+ "config_id": "poll_config_approval/1",
+ "state": Poll.STATE_CREATED,
},
+ "poll_config_approval/1": {"id": 1, "poll_id": 1},
}
)
+ for id_ in range(1, 4):
+ self.set_models(
+ {
+ f"poll_config_option/{id_}": {
+ "id": id_,
+ "weight": id_,
+ "poll_config_id": "poll_config_approval/1",
+ "meeting_user_id": id_ + 1,
+ }
+ }
+ )
+
response = self.request("meeting.clone", {"meeting_id": 1})
self.assert_status_code(response, 200)
+ self.assert_model_exists(
+ "meeting/2",
+ {
+ "committee_id": 60,
+ "list_of_speakers_ids": [2],
+ "assignment_ids": [2],
+ "poll_ids": [2],
+ "projector_ids": [2],
+ "reference_projector_id": 2,
+ **{key: [2] for key in MeetingModelMixin.all_default_projectors()},
+ "motions_default_amendment_workflow_id": 2,
+ },
+ )
+ self.assert_model_exists(
+ "poll/2",
+ {
+ "title": "First election",
+ "meeting_id": 2,
+ "content_object_id": "assignment/2",
+ "visibility": Poll.VISIBILITY_SECRET,
+ "config_id": "poll_config_approval/2",
+ "state": Poll.STATE_CREATED,
+ },
+ )
+ self.assert_model_exists(
+ "poll_config_approval/2",
+ {"poll_id": 2, "option_ids": [4, 5, 6]},
+ )
+ for id_ in range(4, 7):
+ self.assert_model_exists(
+ f"poll_config_option/{id_}",
+ {
+ "weight": id_ - 3,
+ "poll_config_id": "poll_config_approval/2",
+ "meeting_user_id": id_ + 2,
+ },
+ )
def test_clone_with_structured_published_orga_files(self) -> None:
self.set_test_data_with_admin()
diff --git a/tests/system/action/meeting/test_delete.py b/tests/system/action/meeting/test_delete.py
index 56da23e787..fbdb533304 100644
--- a/tests/system/action/meeting/test_delete.py
+++ b/tests/system/action/meeting/test_delete.py
@@ -3,6 +3,7 @@
from psycopg.types.json import Jsonb
+from openslides_backend.models.models import Poll
from openslides_backend.permissions.management_levels import OrganizationManagementLevel
from openslides_backend.permissions.permissions import Permissions
from openslides_backend.shared.util import ONE_ORGANIZATION_FQID
@@ -95,10 +96,15 @@ def test_delete_full_meeting(self) -> None:
self.assert_model_not_exists(f"motion_workflow/{i+1}")
for i in range(5):
self.assert_model_not_exists(f"poll/{i+1}")
- for i in range(13):
- self.assert_model_not_exists(f"option/{i+1}")
+ for i in range(2):
+ self.assert_model_not_exists(f"poll_config_approval/{i+1}")
+ self.assert_model_not_exists("poll_config_approval/1")
+ self.assert_model_not_exists("poll_config_selection/1")
+ self.assert_model_not_exists("poll_config_rating_score/1")
+ for i in range(10):
+ self.assert_model_not_exists(f"poll_config_option/{i+1}")
for i in range(9):
- self.assert_model_not_exists(f"vote/{i+1}")
+ self.assert_model_not_exists(f"ballot/{i+1}")
for i in range(2):
self.assert_model_not_exists(f"assignment/{i+1}")
for i in range(5):
@@ -321,30 +327,19 @@ def test_delete_with_poll_candidates_and_speakers(self) -> None:
"meeting_id": 1,
"content_object_id": "assignment/140",
"title": "Analog poll 150",
- "type": "analog",
- "pollmethod": "YNA",
- "meeting_id": 1,
- },
- "option/160": {
- "meeting_id": 1,
- "poll_id": 150,
- "content_object_id": "poll_candidate_list/170",
- },
- "poll_candidate_list/170": {
+ "state": Poll.STATE_CREATED,
+ "config_id": "poll_config_approval/160",
+ "visibility": Poll.VISIBILITY_MANUALLY,
"meeting_id": 1,
- "option_id": 160,
},
- "poll_candidate/180": {
- "meeting_id": 1,
- "weight": 1,
- "poll_candidate_list_id": 170,
- "user_id": 2,
+ "poll_config_approval/160": {"poll_id": 150},
+ "poll_config_option/180": {
+ "poll_config_id": "poll_config_approval/160",
+ "meeting_user_id": 1,
},
- "poll_candidate/181": {
- "meeting_id": 1,
- "weight": 1,
- "poll_candidate_list_id": 170,
- "user_id": 3,
+ "poll_config_option/181": {
+ "poll_config_id": "poll_config_approval/160",
+ "meeting_user_id": 2,
},
"list_of_speakers/190": {
"meeting_id": 1,
@@ -382,10 +377,9 @@ def test_delete_with_poll_candidates_and_speakers(self) -> None:
"group/3",
"assignment/140",
"poll/150",
- "option/160",
- "poll_candidate_list/170",
- "poll_candidate/180",
- "poll_candidate/181",
+ "poll_config_approval/160",
+ "poll_config_option/180",
+ "poll_config_option/181",
"list_of_speakers/190",
"speaker/210",
"speaker/211",
diff --git a/tests/system/action/meeting/test_import.py b/tests/system/action/meeting/test_import.py
index e322af6eac..c5e808985f 100644
--- a/tests/system/action/meeting/test_import.py
+++ b/tests/system/action/meeting/test_import.py
@@ -8,7 +8,7 @@
from openslides_backend.action.action_worker import ActionWorkerState
from openslides_backend.http.views.presenter_view import PresenterView
from openslides_backend.migrations.migration_helper import MigrationHelper
-from openslides_backend.models.models import Meeting
+from openslides_backend.models.models import Meeting, Poll
from openslides_backend.shared.util import ONE_ORGANIZATION_FQID, get_initial_data_file
from tests.system.action.base import BaseActionTestCase
from tests.system.util import (
@@ -176,8 +176,8 @@ def create_request_data(
"poll_default_method": "votes",
"poll_default_onehundred_percent_base": "valid",
"poll_default_group_ids": [],
- "poll_default_backend": "fast",
"poll_default_live_voting_enabled": False,
+ "poll_default_allow_invalid": False,
"poll_couple_countdown": True,
"projector_ids": [1],
"all_projection_ids": [],
@@ -200,8 +200,6 @@ def create_request_data(
"motion_workflow_ids": [1],
"motion_change_recommendation_ids": [],
"poll_ids": [],
- "option_ids": [],
- "vote_ids": [],
"assignment_ids": [],
"assignment_candidate_ids": [],
"personal_note_ids": [],
@@ -2297,50 +2295,39 @@ def test_import_with_wrong_decimal(self) -> None:
)
def test_import_new_user_with_vote(self) -> None:
+ self.set_user_groups(1, [1])
self.set_models(
{
- "vote/1": {
- "user_id": 1,
- "delegated_user_id": 1,
+ "poll/1": {
+ "title": "pull",
+ "config_id": "poll_config_approval/1",
+ "visibility": Poll.VISIBILITY_MANUALLY,
+ "state": Poll.STATE_STARTED,
"meeting_id": 1,
- "option_id": 10,
- "user_token": "asdfgh",
+ "content_object_id": "meeting/1",
},
- "option/10": {
- "vote_ids": [1],
- "meeting_id": 1,
- },
- "user/1": {
- "vote_ids": [1],
- "delegated_vote_ids": [1],
+ "poll_config_approval/1": {"poll_id": 1},
+ "ballot/1": {
+ "acting_meeting_user_id": 1,
+ "represented_meeting_user_id": 1,
+ "poll_id": 1,
},
}
)
data = self.create_request_data(
{
- "vote": {
- "1": {
- "id": 1,
- "user_id": 1,
- "delegated_user_id": 1,
- "meeting_id": 1,
- "option_id": 1,
- "user_token": "asdfgh",
- },
- },
- "option": {
+ "ballot": {
"1": {
"id": 1,
- "vote_ids": [1],
- "meeting_id": 1,
+ "acting_meeting_user_id": 1,
+ "represented_meeting_user_id": 1,
+ "poll_id": 1,
},
},
}
)
- data["meeting"]["meeting"]["1"]["vote_ids"] = [1]
- data["meeting"]["meeting"]["1"]["option_ids"] = [1]
- data["meeting"]["user"]["1"]["vote_ids"] = [1]
- data["meeting"]["user"]["1"]["delegated_vote_ids"] = [1]
+ data["meeting"]["meeting_user"]["1"]["acting_ballot_ids"] = [1]
+ data["meeting"]["meeting_user"]["1"]["represented_ballot_ids"] = [1]
response = self.request("meeting.import", data)
self.assert_status_code(response, 200)
self.assert_model_exists(
@@ -2348,19 +2335,31 @@ def test_import_new_user_with_vote(self) -> None:
{
"username": "admin",
"meeting_user_ids": [2],
- "vote_ids": [1],
- "delegated_vote_ids": [1],
},
)
self.assert_model_exists(
"user/2",
{
"username": "test",
- "vote_ids": [2],
- "delegated_vote_ids": [2],
"meeting_user_ids": [1],
},
)
+ self.assert_model_exists(
+ "meeting_user/1",
+ {
+ "user_id": 2,
+ "acting_ballot_ids": [1],
+ "represented_ballot_ids": [1],
+ },
+ )
+ self.assert_model_exists(
+ "meeting_user/2",
+ {
+ "user_id": 1,
+ "acting_ballot_ids": [2],
+ "represented_ballot_ids": [2],
+ },
+ )
def test_gender_import(self) -> None:
"""
@@ -2500,52 +2499,40 @@ def test_gender_import(self) -> None:
)
def test_import_existing_user_with_vote(self) -> None:
+ self.set_user_groups(1, [1])
self.set_models(
{
- "vote/1": {
- "user_id": 1,
- "delegated_user_id": 1,
+ "poll/1": {
+ "title": "pull",
+ "config_id": "poll_config_approval/1",
+ "visibility": Poll.VISIBILITY_MANUALLY,
+ "state": Poll.STATE_STARTED,
"meeting_id": 1,
- "option_id": 10,
- "user_token": "asdfgh",
+ "content_object_id": "meeting/1",
},
- "option/10": {
- "vote_ids": [1],
- "meeting_id": 1,
- },
- "user/1": {
- "vote_ids": [1],
- "delegated_vote_ids": [1],
+ "poll_config_approval/1": {"poll_id": 1},
+ "vote/1": {
+ "acting_meeting_user_id": 1,
+ "represented_meeting_user_id": 1,
+ "poll_id": 1,
},
}
)
data = self.create_request_data(
{
- "vote": {
+ "ballot": {
"1": {
"id": 1,
- "user_id": 1,
- "delegated_user_id": 1,
- "meeting_id": 1,
- "option_id": 1,
- "user_token": "asdfgh",
- },
- },
- "option": {
- "1": {
- "id": 1,
- "vote_ids": [1],
- "meeting_id": 1,
+ "acting_meeting_user_id": 1,
+ "represented_meeting_user_id": 1,
},
},
}
)
- data["meeting"]["meeting"]["1"]["vote_ids"] = [1]
- data["meeting"]["meeting"]["1"]["option_ids"] = [1]
data["meeting"]["user"]["1"]["username"] = "admin"
data["meeting"]["user"]["1"]["last_name"] = ""
- data["meeting"]["user"]["1"]["vote_ids"] = [1]
- data["meeting"]["user"]["1"]["delegated_vote_ids"] = [1]
+ data["meeting"]["meeting_user"]["1"]["acting_ballot_ids"] = [1]
+ data["meeting"]["meeting_user"]["1"]["represented_ballot_ids"] = [1]
response = self.request("meeting.import", data)
self.assert_status_code(response, 200)
self.assert_model_exists(
@@ -2553,8 +2540,14 @@ def test_import_existing_user_with_vote(self) -> None:
{
"username": "admin",
"meeting_user_ids": [1],
- "vote_ids": [1, 2],
- "delegated_vote_ids": [1, 2],
+ },
+ )
+ self.assert_model_exists(
+ "meeting_user_ids/1",
+ {
+ "user_id": 1,
+ "acting_ballot_ids": [1, 2],
+ "represented_ballot_ids": [1, 2],
},
)
self.assert_model_not_exists("user/2")
@@ -2728,7 +2721,6 @@ def test_import_export_with_orga_mediafiles(self) -> None:
"assignment_poll_default_backend": "fast",
"poll_default_type": "analog",
"poll_default_onehundred_percent_base": "YNA",
- "poll_default_backend": "fast",
"poll_default_live_voting_enabled": False,
"poll_couple_countdown": True,
**{field: [1] for field in Meeting.all_default_projectors()},
diff --git a/tests/system/action/meeting/test_update.py b/tests/system/action/meeting/test_update.py
index cc4a1d5e19..90421bdc6a 100644
--- a/tests/system/action/meeting/test_update.py
+++ b/tests/system/action/meeting/test_update.py
@@ -281,7 +281,6 @@ def test_update_poll_default_backend_fields(self) -> None:
data = {
"motion_poll_default_backend": "long",
"assignment_poll_default_backend": "long",
- "poll_default_backend": "long",
}
self.basic_test(data)
self.assert_model_exists("meeting/1", data)
@@ -293,6 +292,10 @@ def test_update_poll_default_live_voting_enabled(self) -> None:
{"poll_default_live_voting_enabled": True},
)
+ def test_update_poll_default_allow_invalid(self) -> None:
+ self.basic_test({"poll_default_allow_invalid": True})
+ self.assert_model_exists("meeting/1", {"poll_default_allow_invalid": True})
+
def test_update_motions_block_slide_columns(self) -> None:
self.basic_test({"motions_block_slide_columns": 2})
self.assert_model_exists("meeting/1", {"motions_block_slide_columns": 2})
diff --git a/tests/system/action/meeting_user/test_update.py b/tests/system/action/meeting_user/test_update.py
index 665910336d..4b5717b8a4 100644
--- a/tests/system/action/meeting_user/test_update.py
+++ b/tests/system/action/meeting_user/test_update.py
@@ -1,6 +1,8 @@
from datetime import datetime
from decimal import Decimal
+from typing import Any
+from openslides_backend.models.models import Poll
from tests.system.action.base import BaseActionTestCase
@@ -106,6 +108,84 @@ def test_update_merge_fields_correct(self) -> None:
},
)
+ def test_update_with_vote_related_fields(self) -> None:
+ self.create_meeting()
+ self.set_user_groups(1, [1])
+ self.create_user("dummy2", [1])
+ self.create_user("dummy3", [1])
+ self.set_models(
+ {
+ "meeting_user/1": {
+ "acting_ballot_ids": [1, 2],
+ "represented_ballot_ids": [1],
+ },
+ "poll/1": {
+ "title": "Poll 1",
+ "meeting_id": 1,
+ "content_object_id": "assignment/1",
+ "visibility": Poll.VISIBILITY_NAMED,
+ "config_id": "poll_config_approval/1",
+ "state": Poll.STATE_FINISHED,
+ },
+ "list_of_speakers/1": {
+ "id": 1,
+ "closed": False,
+ "meeting_id": 1,
+ "content_object_id": "assignment/1",
+ "sequential_number": 1,
+ },
+ "assignment/1": {
+ "id": 1,
+ "phase": "search",
+ "title": "Duckburg town council",
+ "meeting_id": 1,
+ "sequential_number": 1,
+ "list_of_speakers_id": 1,
+ },
+ "poll_config_option/1": {
+ "poll_config_id": "poll_config_approval/1",
+ "meeting_user_id": 2,
+ },
+ "poll_config_approval/1": {"poll_id": 1},
+ "ballot/1": {
+ "poll_id": 1,
+ "acting_meeting_user_id": 1,
+ "represented_meeting_user_id": 1,
+ },
+ "ballot/2": {
+ "poll_id": 1,
+ "acting_meeting_user_id": 1,
+ "represented_meeting_user_id": 2,
+ },
+ }
+ )
+ response = self.request(
+ "meeting_user.update",
+ {
+ "id": 3,
+ "poll_option_ids": [1],
+ "poll_voted_ids": [1],
+ "acting_ballot_ids": [1],
+ "represented_ballot_ids": [2],
+ },
+ internal=True,
+ )
+ self.assert_status_code(response, 200)
+ expected: dict[str, dict[str, Any]] = {
+ "meeting_user/3": {
+ "poll_option_ids": [1],
+ "poll_voted_ids": [1],
+ "acting_ballot_ids": [1],
+ "represented_ballot_ids": [2],
+ },
+ "poll/1": {"voted_ids": [3]},
+ "poll_config_option/1": {"meeting_user_id": 3},
+ "ballot/1": {"acting_meeting_user_id": 3},
+ "ballot/2": {"represented_meeting_user_id": 3},
+ }
+ for fqid, model in expected.items():
+ self.assert_model_exists(fqid, model)
+
def test_update_anonymous_group_id(self) -> None:
self.create_meeting()
self.set_models(
diff --git a/tests/system/action/option/__init__.py b/tests/system/action/option/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/tests/system/action/option/test_create.py b/tests/system/action/option/test_create.py
deleted file mode 100644
index 37e297cb79..0000000000
--- a/tests/system/action/option/test_create.py
+++ /dev/null
@@ -1,80 +0,0 @@
-from decimal import Decimal
-
-from tests.system.action.base import BaseActionTestCase
-
-
-class OptionCreateActionTest(BaseActionTestCase):
- def setUp(self) -> None:
- super().setUp()
- self.create_meeting(111)
-
- def test_create(self) -> None:
- response = self.request(
- "option.create", {"text": "testtesttest", "meeting_id": 111, "weight": 10}
- )
- self.assert_status_code(response, 200)
- model = self.get_model("option/1")
- assert model.get("text") == "testtesttest"
- assert model.get("meeting_id") == 111
- assert model.get("weight") == 10
-
- def test_create_without_text_and_content_object_id(self) -> None:
- response = self.request("option.create", {"meeting_id": 111, "weight": 10})
- self.assert_status_code(response, 400)
- assert (
- "Need one of text, content_object_id or poll_candidate_user_ids."
- in response.json["message"]
- )
-
- def test_create_with_both_text_and_content_object_id(self) -> None:
- self.set_models(
- {
- "motion/112": {
- "title": "mosh pit",
- "state_id": 111,
- "meeting_id": 111,
- },
- "list_of_speakers/23": {
- "content_object_id": "motion/112",
- "meeting_id": 111,
- },
- }
- )
- response = self.request(
- "option.create",
- {
- "text": "test",
- "content_object_id": "motion/112",
- "meeting_id": 111,
- "weight": 10,
- },
- )
- self.assert_status_code(response, 400)
- assert (
- "Need one of text, content_object_id or poll_candidate_user_ids."
- in response.json["message"]
- )
-
- def test_create_yna_votes(self) -> None:
- response = self.request(
- "option.create",
- {
- "text": "test",
- "meeting_id": 111,
- "weight": 10,
- "yes": "1.000000",
- "no": "2.500000",
- "abstain": "0.666667",
- },
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists("option/1", {"vote_ids": [1, 2, 3], "text": "test"})
- self.assert_model_exists(
- "vote/1", {"value": "Y", "weight": Decimal("1.000000"), "option_id": 1}
- )
- self.assert_model_exists(
- "vote/2", {"value": "N", "weight": Decimal("2.500000"), "option_id": 1}
- )
- self.assert_model_exists(
- "vote/3", {"value": "A", "weight": Decimal("0.666667"), "option_id": 1}
- )
diff --git a/tests/system/action/option/test_delete.py b/tests/system/action/option/test_delete.py
deleted file mode 100644
index 3b6088c6e3..0000000000
--- a/tests/system/action/option/test_delete.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from tests.system.action.base import BaseActionTestCase
-
-
-class OptionDeleteTest(BaseActionTestCase):
- def setUp(self) -> None:
- super().setUp()
- self.create_meeting()
- self.create_motion(1)
- self.set_models(
- {
- "option/111": {"meeting_id": 1, "content_object_id": "motion/1"},
- }
- )
-
- def test_delete_correct(self) -> None:
- response = self.request("option.delete", {"id": 111})
- self.assert_status_code(response, 200)
- self.assert_model_not_exists("option/111")
- self.assert_model_exists("motion/1")
-
- def test_delete_wrong_id(self) -> None:
- response = self.request("option.delete", {"id": 112})
- self.assert_status_code(response, 400)
- self.assert_model_exists("option/111")
-
- def test_delete_correct_cascading(self) -> None:
- self.set_models(
- {
- "option/112": {
- "meeting_id": 1,
- "content_object_id": "poll_candidate_list/2",
- },
- "vote/42": {
- "user_token": "imnotapropertoken",
- "option_id": 112,
- "meeting_id": 1,
- },
- "poll_candidate_list/2": {"option_id": 112, "meeting_id": 1},
- }
- )
- response = self.request("option.delete", {"id": 112})
- self.assert_status_code(response, 200)
- self.assert_model_not_exists("option/112")
- self.assert_model_not_exists("vote/42")
diff --git a/tests/system/action/option/test_update.py b/tests/system/action/option/test_update.py
deleted file mode 100644
index 2afff49ec4..0000000000
--- a/tests/system/action/option/test_update.py
+++ /dev/null
@@ -1,278 +0,0 @@
-from decimal import Decimal
-from typing import Any
-
-from openslides_backend.permissions.permissions import Permissions
-from tests.system.action.base import BaseActionTestCase
-
-
-class OptionUpdateActionTest(BaseActionTestCase):
- def setUp(self) -> None:
- super().setUp()
- self.vote_models: dict[str, dict[str, Any]] = {
- "option/57": {
- "vote_ids": [22],
- },
- "vote/22": {
- "value": "Y",
- "user_token": "imnotapropertoken",
- "weight": "0.000000",
- "meeting_id": 1,
- "option_id": 57,
- },
- }
- self.create_meeting()
- self.set_models(
- {
- "topic/1": {
- "title": "to pic",
- "meeting_id": 1,
- },
- "agenda_item/1": {"content_object_id": "topic/1", "meeting_id": 1},
- "list_of_speakers/23": {
- "content_object_id": "topic/1",
- "meeting_id": 1,
- },
- "poll/65": {
- "title": "pool",
- "content_object_id": "topic/1",
- "type": "analog",
- "state": "created",
- "pollmethod": "YNA",
- "max_votes_amount": 1,
- "max_votes_per_option": 1,
- "min_votes_amount": 1,
- "meeting_id": 1,
- "option_ids": [57],
- },
- "option/57": {
- "yes": "0.000000",
- "no": "0.000000",
- "abstain": "0.000000",
- "meeting_id": 1,
- "poll_id": 65,
- },
- }
- )
-
- def test_update(self) -> None:
- self.set_models(self.vote_models)
- response = self.request(
- "option.update",
- {"id": 57, "Y": "1.000000", "N": "2.000000", "A": "3.000000"},
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "option/57",
- {
- "yes": Decimal("1.000000"),
- "no": Decimal("2.000000"),
- "abstain": Decimal("3.000000"),
- "vote_ids": [22, 23, 24],
- },
- )
- self.assert_model_exists(
- "vote/22", {"option_id": 57, "value": "Y", "weight": Decimal("1.000000")}
- )
- self.assert_model_exists(
- "vote/23", {"option_id": 57, "value": "N", "weight": Decimal("2.000000")}
- )
- self.assert_model_exists(
- "vote/24", {"option_id": 57, "value": "A", "weight": Decimal("3.000000")}
- )
- poll = self.get_model("poll/65")
- assert poll.get("state") == "finished"
-
- def test_update_Y(self) -> None:
- response = self.request(
- "option.update",
- {
- "id": 57,
- "Y": "1.000000",
- "N": "2.000000",
- "A": "3.000000",
- "publish_immediately": True,
- },
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "option/57",
- {
- "yes": Decimal("1.000000"),
- "no": Decimal("2.000000"),
- "abstain": Decimal("3.000000"),
- "publish_immediately": None,
- },
- )
- poll = self.get_model("poll/65")
- assert poll.get("state") == "published"
-
- def test_update_default_values(self) -> None:
- self.set_models(
- {
- "poll/65": {
- "pollmethod": "YN",
- },
- "option/57": {
- "abstain": None,
- },
- }
- )
- response = self.request(
- "option.update",
- {
- "id": 57,
- "Y": "1.000000",
- },
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "option/57",
- {"yes": Decimal("1.000000"), "no": Decimal("-2.000000"), "abstain": None},
- )
-
- def test_update_invalid_keys(self) -> None:
- self.set_models(
- {
- "poll/65": {
- "pollmethod": "YN",
- },
- }
- )
- response = self.request(
- "option.update",
- {
- "id": 57,
- "A": "0.000000",
- },
- )
- self.assert_status_code(response, 400)
- assert (
- "Pollmethod YN does not support abstain votes" in response.json["message"]
- )
-
- def test_update_global_option(self) -> None:
- self.set_models(
- {
- "poll/65": {
- "pollmethod": "Y",
- "global_option_id": 57,
- "global_yes": True,
- "global_no": True,
- "global_abstain": True,
- },
- "option/57": {"used_as_global_option_in_poll_id": 65},
- }
- )
- response = self.request(
- "option.update",
- {"id": 57, "Y": "1.000000", "N": "2.000000"},
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "option/57",
- {
- "yes": Decimal("1.000000"),
- "no": Decimal("2.000000"),
- "abstain": Decimal("-2.000000"),
- },
- )
-
- def test_update_global_option_invalid(self) -> None:
- self.set_models({"option/57": {"used_as_global_option_in_poll_id": 65}})
- response = self.request(
- "option.update",
- {"id": 57, "Y": "1.000000"},
- )
- self.assert_status_code(response, 400)
- assert (
- "Global yes votes are not allowed for this poll" in response.json["message"]
- )
-
- def test_update_no_permissions(self) -> None:
- self.base_permission_test(
- self.vote_models,
- "option.update",
- {"id": 57, "Y": "1.000000", "N": "2.000000", "A": "3.000000"},
- )
-
- def test_update_permissions(self) -> None:
- self.base_permission_test(
- self.vote_models,
- "option.update",
- {"id": 57, "Y": "1.000000", "N": "2.000000", "A": "3.000000"},
- Permissions.Poll.CAN_MANAGE,
- )
-
- def test_update_permissions_locked_meeting(self) -> None:
- self.base_locked_out_superadmin_permission_test(
- self.vote_models,
- "option.update",
- {"id": 57, "Y": "1.000000", "N": "2.000000", "A": "3.000000"},
- )
-
- def test_update_together_with_poll(self) -> None:
- self.set_models(
- {
- "poll/65": {
- "pollmethod": "YN",
- "option_ids": [57, 58],
- },
- "option/58": {
- "yes": "0.000000",
- "no": "0.000000",
- "abstain": "0.000000",
- "meeting_id": 1,
- "poll_id": 65,
- },
- }
- )
- response = self.request_json(
- [
- {
- "action": "poll.update",
- "data": [
- {
- "id": 65,
- "onehundred_percent_base": "valid",
- "pollmethod": "YNA",
- "title": "Ballot",
- }
- ],
- },
- {
- "action": "option.update",
- "data": [
- {"id": 57, "Y": "1.000000", "A": "3.000000", "N": "2.000000"},
- {"id": 58, "Y": "4.000000", "N": "-1.000000"},
- ],
- },
- ]
- )
- self.assert_status_code(response, 200)
-
- def test_update_together_with_poll_2(self) -> None:
- response = self.request_json(
- [
- {
- "action": "poll.update",
- "data": [
- {
- "id": 65,
- "onehundred_percent_base": "YNA",
- "pollmethod": "YNA",
- "title": "Abstimmung",
- "votescast": "10.000000",
- "votesinvalid": "10.000000",
- "votesvalid": "10.000000",
- }
- ],
- },
- {
- "action": "option.update",
- "data": [
- {"id": 57, "Y": "10.000000", "A": "10.000000", "N": "10.000000"}
- ],
- },
- ]
- )
- self.assert_status_code(response, 200)
diff --git a/tests/system/action/organization/test_delete_history_information.py b/tests/system/action/organization/test_delete_history_information.py
index c5468519c7..7177fadc47 100644
--- a/tests/system/action/organization/test_delete_history_information.py
+++ b/tests/system/action/organization/test_delete_history_information.py
@@ -1,3 +1,4 @@
+from openslides_backend.models.models import Poll
from openslides_backend.permissions.management_levels import OrganizationManagementLevel
from tests.system.action.base import BaseActionTestCase
@@ -26,18 +27,19 @@ def test_delete_history_information_correct(self) -> None:
},
}
)
- response = self.request(
- "poll.create",
+ vote_service_response = self.vote_service.create(
{
"title": "test",
- "type": "analog",
- "pollmethod": "Y",
- "options": [{"text": "test2", "Y": "10.000000"}],
+ "visibility": Poll.VISIBILITY_MANUALLY,
+ "method": Poll.METHOD_RATING_APPROVAL,
+ "state": Poll.STATE_CREATED,
"meeting_id": 1,
"content_object_id": "assignment/1",
+ "config": {"allow_abstain": True},
+ "result": {"yes": "3", "no": "2", "abstain": "1"},
},
)
- self.assert_status_code(response, 200)
+ self.assertIsNotNone(vote_service_response)
self.assert_history_information("assignment/1", ["Ballot created"])
for collection in ["history_position", "history_entry"]:
self.assert_model_exists(f"{collection}/1")
diff --git a/tests/system/action/poll/__init__.py b/tests/system/action/poll/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/tests/system/action/poll/poll_test_mixin.py b/tests/system/action/poll/poll_test_mixin.py
deleted file mode 100644
index b13691987b..0000000000
--- a/tests/system/action/poll/poll_test_mixin.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from openslides_backend.models.models import Poll
-from tests.system.action.base import BaseActionTestCase
-from tests.system.base import ADMIN_PASSWORD, ADMIN_USERNAME, DEFAULT_PASSWORD
-
-
-class PollTestMixin(BaseActionTestCase):
- def start_poll(self, id: int) -> None:
- self.vote_service.start(id)
-
- def prepare_users_and_poll(self, user_count: int) -> list[int]:
- user_ids = list(range(2, user_count + 2))
- self.create_meeting()
- self.create_motion(1, 1)
- self.set_models(
- {
- "poll/1": {
- "content_object_id": "motion/1",
- "type": Poll.TYPE_NAMED,
- "pollmethod": "YNA",
- "backend": "fast",
- "state": Poll.STATE_STARTED,
- "meeting_id": 1,
- "onehundred_percent_base": "YNA",
- "title": "Poll 1",
- },
- "option/1": {"meeting_id": 1, "poll_id": 1},
- **{f"user/{i}": {**self._get_user_data(f"user{i}")} for i in user_ids},
- **{
- f"meeting_user/{i+10}": {"meeting_id": 1, "user_id": i}
- for i in user_ids
- },
- "group/3": {
- "meeting_user_ids": [id_ + 10 for id_ in user_ids],
- "poll_ids": [1],
- },
- "meeting/1": {"present_user_ids": user_ids},
- }
- )
- self.start_poll(1)
- for i in user_ids:
- self.client.login(f"user{i}", DEFAULT_PASSWORD)
- response = self.vote_service.vote({"id": 1, "value": {"1": "Y"}})
- self.assert_status_code(response, 200)
- self.client.login(ADMIN_USERNAME, ADMIN_PASSWORD)
- return user_ids
diff --git a/tests/system/action/poll/test_anonymize.py b/tests/system/action/poll/test_anonymize.py
deleted file mode 100644
index 67d9a08c39..0000000000
--- a/tests/system/action/poll/test_anonymize.py
+++ /dev/null
@@ -1,139 +0,0 @@
-from typing import Any
-
-from openslides_backend.models.models import Poll
-from openslides_backend.permissions.permissions import Permissions
-from openslides_backend.services.database.interface import PartialModel
-
-from .base_poll_test import BasePollTestCase
-
-
-class PollAnonymize(BasePollTestCase):
- def setUp(self) -> None:
- super().setUp()
- self.create_meeting()
- self.create_topic(1, 1)
- self.set_user_groups(1, [1])
- self.test_data: dict[str, dict[str, Any]] = {
- "poll/1": {
- "title": "Poll 1",
- "global_option_id": 2,
- "meeting_id": 1,
- "state": Poll.STATE_FINISHED,
- "type": Poll.TYPE_NAMED,
- "content_object_id": "topic/1",
- "pollmethod": "Y",
- "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_Y,
- },
- "option/1": {"meeting_id": 1, "poll_id": 1},
- "option/2": {"meeting_id": 1, "poll_id": 1},
- "vote/1": {
- "user_id": 1,
- "meeting_id": 1,
- "delegated_user_id": 1,
- "user_token": "abc",
- "option_id": 1,
- },
- "vote/2": {
- "user_id": 1,
- "meeting_id": 1,
- "delegated_user_id": 1,
- "user_token": "edf",
- "option_id": 2,
- },
- }
-
- def set_test_data(self, poll_data: PartialModel = {}) -> None:
- if poll_data:
- self.test_data["poll/1"].update(poll_data)
- self.set_models(self.test_data)
-
- def assert_anonymize(self) -> None:
- self.assert_model_exists("poll/1", {"is_pseudoanonymized": True})
- for fqid in ("vote/1", "vote/2"):
- self.assert_model_exists(fqid, {"user_id": None, "delegated_user_id": None})
- self.assert_model_exists(
- "user/1", {"vote_ids": None, "delegated_vote_ids": None}
- )
-
- def test_anonymize(self) -> None:
- self.set_test_data()
- response = self.request("poll.anonymize", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_anonymize()
- self.assert_history_information("topic/1", None)
-
- def test_anonymize_motion_poll(self) -> None:
- self.create_motion(1, 11)
- self.set_models(
- {
- "poll/1": {
- "title": "Poll 1",
- "meeting_id": 1,
- "type": Poll.TYPE_NAMED,
- "state": Poll.STATE_FINISHED,
- "content_object_id": "motion/11",
- "pollmethod": "Y",
- "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_Y,
- },
- }
- )
- response = self.request("poll.anonymize", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_history_information("motion/11", ["Voting anonymized"])
-
- def test_anonymize_assignment_poll(self) -> None:
- self.create_assignment(1, 1)
- self.set_test_data({"content_object_id": "assignment/1"})
- response = self.request("poll.anonymize", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_history_information("assignment/1", ["Ballot anonymized"])
-
- def test_anonymize_publish_state(self) -> None:
- self.set_test_data({"state": Poll.STATE_PUBLISHED})
- response = self.request("poll.anonymize", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_anonymize()
-
- def test_anonymize_wrong_state(self) -> None:
- self.set_test_data({"state": Poll.STATE_CREATED})
- response = self.request("poll.anonymize", {"id": 1})
- self.assert_status_code(response, 400)
- for vote_fqid in ("vote/1", "vote/2"):
- self.assert_model_exists(vote_fqid, {"user_id": 1, "delegated_user_id": 1})
- self.assertEqual(
- "Anonymizing can only be done after finishing a poll.",
- response.json["message"],
- )
-
- def test_anonymize_wrong_type(self) -> None:
- self.set_test_data({"type": Poll.TYPE_ANALOG})
- response = self.request("poll.anonymize", {"id": 1})
- self.assert_status_code(response, 400)
- for vote_fqid in ("vote/1", "vote/2"):
- self.assert_model_exists(vote_fqid, {"user_id": 1, "delegated_user_id": 1})
- self.assertEqual(
- "You can only anonymize named polls.",
- response.json["message"],
- )
-
- def test_anonymize_no_permissions(self) -> None:
- self.base_permission_test(
- self.test_data,
- "poll.anonymize",
- {"id": 1},
- )
-
- def test_anonymize_permissions(self) -> None:
- self.base_permission_test(
- self.test_data,
- "poll.anonymize",
- {"id": 1},
- Permissions.Poll.CAN_MANAGE,
- )
-
- def test_anonymize_permissions_locked_meeting(self) -> None:
- self.base_locked_out_superadmin_permission_test(
- self.test_data,
- "poll.anonymize",
- {"id": 1},
- )
diff --git a/tests/system/action/poll/test_create.py b/tests/system/action/poll/test_create.py
deleted file mode 100644
index 246d5be5de..0000000000
--- a/tests/system/action/poll/test_create.py
+++ /dev/null
@@ -1,1210 +0,0 @@
-import typing
-from decimal import Decimal
-
-from openslides_backend.models.models import Poll
-from openslides_backend.permissions.permissions import Permissions
-from openslides_backend.shared.util import ONE_ORGANIZATION_FQID
-
-from .base_poll_test import BasePollTestCase
-
-
-class CreatePoll(BasePollTestCase):
- def setUp(self) -> None:
- super().setUp()
- self.create_meeting()
- self.create_assignment(1, 1, {"open_posts": 1})
-
- def test_create_correct(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "content_object_id": "assignment/1",
- "pollmethod": "Y",
- "options": [{"text": "test2", "Y": "10.000000"}],
- "meeting_id": 1,
- "global_yes": True,
- "global_no": True,
- "global_abstain": True,
- "onehundred_percent_base": "Y",
- "min_votes_amount": 5,
- "max_votes_amount": 10,
- "max_votes_per_option": 1,
- "backend": "long",
- "amount_global_yes": "2.000000",
- },
- )
- self.assert_status_code(response, 200)
- assert response.json["results"][0][0] == {"id": 1, "sequential_number": 1}
- poll = self.assert_model_exists(
- "poll/1",
- {
- "title": "test",
- "type": "analog",
- "content_object_id": "assignment/1",
- "pollmethod": "Y",
- "meeting_id": 1,
- "option_ids": [1],
- "global_option_id": 2,
- "state": "finished",
- "onehundred_percent_base": "Y",
- "is_pseudoanonymized": False,
- "min_votes_amount": 5,
- "max_votes_amount": 10,
- "max_votes_per_option": 1,
- "backend": "long",
- "sequential_number": 1,
- },
- )
- assert "options" not in poll
- self.assert_model_exists(
- "option/1",
- {
- "text": "test2",
- "poll_id": 1,
- "meeting_id": 1,
- "yes": Decimal("10.000000"),
- },
- )
- self.assert_model_exists(
- "option/2",
- {
- "text": "global option",
- "used_as_global_option_in_poll_id": 1,
- "meeting_id": 1,
- "yes": Decimal("2.000000"),
- "no": Decimal("-2.000000"),
- },
- )
- self.assert_history_information("assignment/1", ["Ballot created"])
-
- def test_create_correct_publish_immediately(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "content_object_id": "assignment/1",
- "pollmethod": "Y",
- "options": [{"text": "test2", "Y": "10.000000"}],
- "meeting_id": 1,
- "global_yes": True,
- "global_no": True,
- "global_abstain": True,
- "onehundred_percent_base": "Y",
- "min_votes_amount": 5,
- "max_votes_amount": 10,
- "max_votes_per_option": 1,
- "publish_immediately": True,
- },
- )
- self.assert_status_code(response, 200)
- poll = self.assert_model_exists("poll/1", {"state": "published"})
- assert "publish_immediately" not in poll
-
- def test_create_correct_default_values(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "content_object_id": "assignment/1",
- "pollmethod": "YN",
- "options": [{"text": "test2", "Y": "10.000000"}],
- "meeting_id": 1,
- "onehundred_percent_base": "Y",
- "votesvalid": "3.000000",
- },
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1",
- {"votesvalid": Decimal("3.000000"), "votesinvalid": Decimal("-2.000000")},
- )
- self.assert_model_exists(
- "vote/1", {"value": "Y", "weight": Decimal("10.000000")}
- )
- self.assert_model_exists(
- "vote/2", {"value": "N", "weight": Decimal("-2.000000")}
- )
-
- def test_create_correct_with_topic(self) -> None:
- self.create_topic(12, 1, {"title": "Wichtiges Topic", "text": "blablabla"})
- response = self.request(
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "content_object_id": "topic/12",
- "pollmethod": "Y",
- "options": [{"text": "test2", "Y": "10.000000"}],
- "meeting_id": 1,
- "global_yes": True,
- "global_no": True,
- "global_abstain": True,
- "onehundred_percent_base": "Y",
- "min_votes_amount": 5,
- "max_votes_amount": 10,
- "max_votes_per_option": 1,
- "publish_immediately": True,
- },
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists("poll/1", {"content_object_id": "topic/12"})
- self.assert_model_exists(
- "topic/12",
- {
- "title": "Wichtiges Topic",
- "text": "blablabla",
- "meeting_id": 1,
- "poll_ids": [1],
- },
- )
-
- def test_create_three_options(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "pollmethod": "YNA",
- "options": [
- {"text": "test2", "Y": "10.000000"},
- {"text": "test3", "N": "0.999900"},
- {"text": "test4", "N": "11.000000"},
- ],
- "meeting_id": 1,
- "onehundred_percent_base": "YNA",
- "content_object_id": "assignment/1",
- "amount_global_yes": "5.000000",
- },
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1",
- {
- "title": "test",
- "type": "analog",
- "pollmethod": "YNA",
- "meeting_id": 1,
- "option_ids": [1, 2, 3],
- "global_option_id": 4,
- "onehundred_percent_base": "YNA",
- },
- )
- self.assert_model_exists(
- "option/1",
- {
- "text": "test2",
- "poll_id": 1,
- "meeting_id": 1,
- "yes": Decimal("10.000000"),
- "weight": 1,
- },
- )
- self.assert_model_exists(
- "option/2",
- {
- "text": "test3",
- "poll_id": 1,
- "meeting_id": 1,
- "no": Decimal("0.999900"),
- "weight": 2,
- },
- )
- self.assert_model_exists(
- "option/3",
- {
- "text": "test4",
- "poll_id": 1,
- "meeting_id": 1,
- "no": Decimal("11.000000"),
- "weight": 3,
- },
- )
- self.assert_model_exists(
- "option/4",
- {
- "text": "global option",
- "used_as_global_option_in_poll_id": 1,
- "meeting_id": 1,
- "weight": 1,
- "yes": None,
- },
- )
-
- def test_all_fields(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test_title_ahThai4pae1pi4xoogoo",
- "pollmethod": "YN",
- "type": "pseudoanonymous",
- "content_object_id": "assignment/1",
- "onehundred_percent_base": "YN",
- "global_yes": False,
- "global_no": False,
- "global_abstain": False,
- "description": "test_description_ieM8ThuasoSh8aecai8p",
- "meeting_id": 1,
- "options": [{"text": "test"}],
- },
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1",
- {
- "title": "test_title_ahThai4pae1pi4xoogoo",
- "description": "test_description_ieM8ThuasoSh8aecai8p",
- "type": Poll.TYPE_PSEUDOANONYMOUS,
- "pollmethod": "YN",
- "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_YN,
- "is_pseudoanonymized": True,
- "option_ids": [1],
- "global_yes": False,
- "global_no": False,
- "global_abstain": False,
- },
- )
- self.assert_model_exists(
- "option/1",
- {
- "text": "test",
- "poll_id": 1,
- "meeting_id": 1,
- "weight": 1,
- },
- )
-
- def test_create_wrong_publish_immediately(self) -> None:
- response = self.request(
- "poll.create",
- {
- "content_object_id": "assignment/1",
- "title": "test_title_ahThai4pae1pi4xoogoo",
- "pollmethod": "YN",
- "type": "pseudoanonymous",
- "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_YN,
- "meeting_id": 1,
- "options": [{"text": "test"}],
- "publish_immediately": True,
- },
- )
- self.assert_status_code(response, 400)
- self.assertEqual(
- "publish_immediately only allowed for analog polls.",
- response.json["message"],
- )
- self.assert_model_not_exists("poll/1")
-
- def test_no_options(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test_title_eing5eipue5cha2Iefai",
- "pollmethod": "YNA",
- "type": "named",
- "content_object_id": "assignment/1",
- "onehundred_percent_base": "YN",
- "meeting_id": 1,
- "options": [],
- },
- )
- self.assert_status_code(response, 400)
- self.assertEqual(
- "Action poll.create: data.options must contain at least 1 items",
- response.json["message"],
- )
- self.assert_model_not_exists("poll/1")
-
- def test_invalid_options(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test_title_eing5eipue5cha2Iefai",
- "pollmethod": "YNA",
- "type": "named",
- "content_object_id": "assignment/1",
- "onehundred_percent_base": "YN",
- "meeting_id": 1,
- "options": [{}],
- },
- )
- self.assert_status_code(response, 400)
- self.assertIn(
- "Need one of text, content_object_id or poll_candidate_user_ids.",
- response.json["message"],
- )
- self.assert_model_not_exists("poll/1")
-
- def test_missing_keys(self) -> None:
- complete_request_data = {
- "title": "test_title_keugh8Iu9ciyooGaevoh",
- "pollmethod": "YNA",
- "type": "named",
- "meeting_id": 1,
- "options": [{"text": "test"}],
- }
- for key in complete_request_data.keys():
- request_data = {
- _key: value
- for _key, value in complete_request_data.items()
- if _key != key
- }
- response = self.request("poll.create", request_data)
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("poll/1")
-
- def test_with_groups(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test_title_Thoo2eiphohhi1eeXoow",
- "pollmethod": "YNA",
- "type": "named",
- "content_object_id": "assignment/1",
- "onehundred_percent_base": "YN",
- "entitled_group_ids": [1, 2],
- "meeting_id": 1,
- "options": [{"text": "test"}],
- },
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists("poll/1", {"entitled_group_ids": [1, 2]})
-
- def test_with_empty_groups(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test_title_Thoo2eiphohhi1eeXoow",
- "pollmethod": "YNA",
- "type": "named",
- "content_object_id": "assignment/1",
- "onehundred_percent_base": "YN",
- "entitled_group_ids": [],
- "meeting_id": 1,
- "options": [{"text": "test"}],
- },
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists("poll/1", {"entitled_group_ids": None})
-
- def test_with_groups_and_analog(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test_title_Thoo2eiphohhi1eeXoow",
- "pollmethod": "YNA",
- "type": "analog",
- "content_object_id": "assignment/1",
- "onehundred_percent_base": "YN",
- "entitled_group_ids": [1, 2],
- "meeting_id": 1,
- "options": [{"text": "test"}],
- },
- )
- self.assert_status_code(response, 400)
- self.assertEqual(
- "entitled_group_ids is not allowed for analog.", response.json["message"]
- )
-
- def test_with_100_percent_base_entitled_and_analog(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test_title_Thoo2eiphohhi1eeXoow",
- "pollmethod": "YNA",
- "type": "analog",
- "content_object_id": "assignment/1",
- "onehundred_percent_base": "entitled",
- "meeting_id": 1,
- "options": [{"text": "test"}],
- },
- )
- self.assert_status_code(response, 400)
- self.assertEqual(
- "onehundred_percent_base: value entitled is not allowed for analog.",
- response.json["message"],
- )
-
- def test_not_supported_type(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test_title_yaiyeighoh0Iraet3Ahc",
- "pollmethod": "YNA",
- "type": "not_existing",
- "content_object_id": "assignment/1",
- "onehundred_percent_base": "YN",
- "meeting_id": 1,
- "options": [{"text": "test"}],
- },
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("poll/1")
-
- def test_not_allowed_type(self) -> None:
- self.update_model(ONE_ORGANIZATION_FQID, {"enable_electronic_voting": False})
- response = self.request(
- "poll.create",
- {
- "title": "test_title_yaiyeighoh0Iraet3Ahc",
- "pollmethod": "YNA",
- "type": Poll.TYPE_NAMED,
- "content_object_id": "assignment/1",
- "onehundred_percent_base": "YN",
- "meeting_id": 1,
- "options": [{"text": "test"}],
- },
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("poll/1")
-
- def test_not_supported_pollmethod(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test_title_SeVaiteYeiNgie5Xoov8",
- "pollmethod": "not_existing",
- "type": "named",
- "content_object_id": "assignment/1",
- "onehundred_percent_base": "YN",
- "meeting_id": 1,
- "options": [{"text": "test"}],
- },
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("poll/1")
-
- def test_not_supported_onehundred_percent_base(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test_title_Thoo2eiphohhi1eeXoow",
- "pollmethod": "YNA",
- "type": "named",
- "content_object_id": "assignment/1",
- "onehundred_percent_base": "invalid base",
- "meeting_id": 1,
- "options": [{"text": "test"}],
- },
- )
- self.assert_status_code(response, 400)
- self.assertIn(
- "data.onehundred_percent_base must be one of ['Y', 'YN', 'YNA', 'N', 'valid', 'cast', 'entitled', 'entitled_present', 'disabled']",
- response.json["message"],
- )
- self.assert_model_not_exists("poll/1")
-
- def test_wrong_pollmethod_onehundred_percent_base_combination_1(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test_title_Thoo2eiphohhi1eeXoow",
- "pollmethod": "Y",
- "type": "named",
- "content_object_id": "assignment/1",
- "onehundred_percent_base": "YN",
- "meeting_id": 1,
- "options": [{"text": "test"}],
- },
- )
- self.assert_status_code(response, 400)
- self.assertEqual(
- "This onehundred_percent_base not allowed in this pollmethod.",
- response.json["message"],
- )
- self.assert_model_not_exists("poll/1")
-
- def test_wrong_pollmethod_onehundred_percent_base_combination_2(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test_title_Thoo2eiphohhi1eeXoow",
- "pollmethod": "YN",
- "type": "named",
- "content_object_id": "assignment/1",
- "onehundred_percent_base": "YNA",
- "meeting_id": 1,
- "options": [{"text": "test"}],
- },
- )
- self.assert_status_code(response, 400)
- self.assertEqual(
- "This onehundred_percent_base not allowed in this pollmethod.",
- response.json["message"],
- )
- self.assert_model_not_exists("poll/1")
-
- def test_wrong_pollmethod_onehundred_percent_base_combination_3(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test_title_Thoo2eiphohhi1eeXoow",
- "pollmethod": "Y",
- "type": "named",
- "content_object_id": "assignment/1",
- "onehundred_percent_base": "YNA",
- "meeting_id": 1,
- "options": [{"text": "test"}],
- },
- )
- self.assert_status_code(response, 400)
- self.assertEqual(
- "This onehundred_percent_base not allowed in this pollmethod.",
- response.json["message"],
- )
- self.assert_model_not_exists("poll/1")
-
- def test_create_poll_for_option_with_wrong_content_object(self) -> None:
- response = self.request(
- "poll.create",
- {
- "meeting_id": 1,
- "title": "Wahlgang (3)",
- "onehundred_percent_base": "valid",
- "pollmethod": "YN",
- "type": "analog",
- "options": [{"content_object_id": "assignment/1"}],
- "content_object_id": "assignment/1",
- },
- )
- self.assert_status_code(response, 400)
- self.assertEqual(
- "The collection 'assignment' is not available for field 'content_object_id' in collection 'option'.",
- response.json["message"],
- )
-
- def test_unique_error_options_text(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "pollmethod": "YNA",
- "onehundred_percent_base": "valid",
- "options": [
- {"text": "test", "Y": "10.000000"},
- {"text": "test", "A": "11.000000"},
- {"text": "test", "N": "12.000000"},
- ],
- "meeting_id": 1,
- "content_object_id": "assignment/1",
- },
- )
- self.assert_status_code(response, 400)
- self.assertIn(
- "Duplicated option in poll.options: test", response.json["message"]
- )
-
- def test_unique_error_options_content_object_id(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "pollmethod": "YNA",
- "options": [
- {"content_object_id": "user/1", "Y": "10.000000"},
- {"text": "test4", "N": "11.000000"},
- {"content_object_id": "user/1", "Y": "11.000000"},
- ],
- "meeting_id": 1,
- "content_object_id": "assignment/1",
- },
- )
- self.assert_status_code(response, 400)
- self.assertIn(
- "Duplicated option in poll.options: user/1", response.json["message"]
- )
-
- def test_unique_no_error_mixed_text_content_object_id_options(self) -> None:
- self.set_user_groups(1, [1])
- response = self.request(
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "pollmethod": "YN",
- "onehundred_percent_base": "valid",
- "options": [
- {
- "content_object_id": "user/1",
- "Y": "10.000000",
- "N": "5.000000",
- },
- {"text": "text", "Y": "10.000000"},
- ],
- "meeting_id": 1,
- "content_object_id": "assignment/1",
- },
- )
- self.assert_status_code(response, 200)
-
- def test_analog_poll_without_YNA_values(self) -> None:
- self.create_motion(1, 3)
- self.set_models({"motion_state/1": {"allow_create_poll": True}})
- response = self.request(
- "poll.create",
- {
- "meeting_id": 1,
- "title": "Abstimmung",
- "onehundred_percent_base": "YNA",
- "pollmethod": "YNA",
- "type": "analog",
- "options": [{"content_object_id": "motion/3"}],
- "content_object_id": "motion/3",
- },
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "option/1",
- {
- "content_object_id": "motion/3",
- "vote_ids": [1, 2, 3],
- "yes": Decimal("-2.000000"),
- "no": Decimal("-2.000000"),
- "abstain": Decimal("-2.000000"),
- "weight": 1,
- },
- )
- self.assert_model_exists(
- "option/2",
- {
- "text": "global option",
- "used_as_global_option_in_poll_id": 1,
- "weight": 1,
- },
- )
- self.assert_history_information("motion/3", ["Voting created"])
-
- def test_not_state_change(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test_title_eing5eipue5cha2Iefai",
- "pollmethod": "YNA",
- "type": "named",
- "content_object_id": "assignment/1",
- "onehundred_percent_base": "YN",
- "meeting_id": 1,
- "options": [{"text": "test1"}],
- },
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists("poll/1", {"state": "created"})
-
- def test_create_user_option_valid(self) -> None:
- self.set_user_groups(1, [1])
- response = self.request(
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "pollmethod": "YNA",
- "options": [{"content_object_id": "user/1"}],
- "meeting_id": 1,
- "onehundred_percent_base": "YN",
- "content_object_id": "assignment/1",
- },
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1",
- {"option_ids": [1], "meeting_id": 1},
- )
- self.assert_model_exists(
- "option/1",
- {"content_object_id": "user/1", "poll_id": 1, "meeting_id": 1},
- )
-
- def test_create_user_option_invalid(self) -> None:
- self.create_meeting(7)
- self.create_meeting(42)
- self.set_user_groups(1, [42])
-
- response = self.request(
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "pollmethod": "YNA",
- "options": [{"content_object_id": "user/1"}],
- "meeting_id": 7,
- "onehundred_percent_base": "YN",
- "content_object_id": "assignment/1",
- },
- )
- self.assert_status_code(response, 400)
- self.assertEqual(
- "The following models do not belong to meeting 7: ['user/1']",
- response.json["message"],
- )
-
- def test_create_without_content_object(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test_title_eing5eipue5cha2Iefai",
- "pollmethod": "YNA",
- "type": "named",
- "onehundred_percent_base": "YN",
- "meeting_id": 1,
- "options": [{"text": "test1"}],
- },
- )
- self.assert_status_code(response, 400)
- self.assertEqual("No 'content_object_id' was given", response.json["message"])
-
- def test_create_no_permissions_assignment(self) -> None:
- self.base_permission_test(
- {},
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "content_object_id": "assignment/1",
- "pollmethod": "Y",
- "options": [{"text": "test2", "Y": "10.000000"}],
- "meeting_id": 1,
- "global_yes": True,
- "global_no": True,
- "global_abstain": True,
- "onehundred_percent_base": "Y",
- },
- )
-
- def test_create_permissions_assignment(self) -> None:
- self.base_permission_test(
- {},
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "content_object_id": "assignment/1",
- "pollmethod": "Y",
- "options": [{"text": "test2", "Y": "10.000000"}],
- "meeting_id": 1,
- "global_yes": True,
- "global_no": True,
- "global_abstain": True,
- "onehundred_percent_base": "Y",
- },
- Permissions.Assignment.CAN_MANAGE_POLLS,
- )
-
- def test_create_permissions_assignment_not_working_with_just_can_manage(
- self,
- ) -> None:
- self.base_permission_test(
- {},
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "content_object_id": "assignment/1",
- "pollmethod": "Y",
- "options": [{"text": "test2", "Y": "10.000000"}],
- "meeting_id": 1,
- "global_yes": True,
- "global_no": True,
- "global_abstain": True,
- "onehundred_percent_base": "Y",
- },
- Permissions.Assignment.CAN_MANAGE,
- fail=True,
- )
-
- def test_create_permissions_assignment_locked_meeting(self) -> None:
- self.base_locked_out_superadmin_permission_test(
- {},
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "content_object_id": "assignment/1",
- "pollmethod": "Y",
- "options": [{"text": "test2", "Y": "10.000000"}],
- "meeting_id": 1,
- "global_yes": True,
- "global_no": True,
- "global_abstain": True,
- "onehundred_percent_base": "Y",
- },
- )
-
- def test_create_forbidden_to_create_poll(self) -> None:
- self.create_motion(1, 23)
- self.set_models({"motion_state/1": {"allow_create_poll": False}})
- response = self.request(
- "poll.create",
- {
- "meeting_id": 1,
- "title": "Abstimmung",
- "onehundred_percent_base": "YNA",
- "pollmethod": "YNA",
- "type": "analog",
- "options": [{"content_object_id": "motion/23"}],
- "content_object_id": "motion/23",
- },
- )
- self.assert_status_code(response, 400)
- self.assertEqual(
- "Motion state doesn't allow to create poll.", response.json["message"]
- )
-
- def test_create_no_permissions_motion(self) -> None:
- self.create_motion(1, 23)
- self.base_permission_test(
- {"motion_state/1": {"allow_create_poll": False}},
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "content_object_id": "motion/23",
- "pollmethod": "Y",
- "options": [{"text": "test2", "Y": "10.000000"}],
- "meeting_id": 1,
- "global_yes": True,
- "global_no": True,
- "global_abstain": True,
- "onehundred_percent_base": "Y",
- },
- )
-
- def test_create_permissions_motion(self) -> None:
- self.create_motion(1, 23)
- self.base_permission_test(
- {"motion_state/1": {"allow_create_poll": True}},
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "content_object_id": "motion/23",
- "pollmethod": "Y",
- "options": [{"text": "test2", "Y": "10.000000"}],
- "meeting_id": 1,
- "global_yes": True,
- "global_no": True,
- "global_abstain": True,
- "onehundred_percent_base": "Y",
- },
- Permissions.Motion.CAN_MANAGE_POLLS,
- )
-
- def test_create_no_permissions_topic(self) -> None:
- self.create_topic(13, 1)
- self.base_permission_test(
- {},
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "pollmethod": "Y",
- "options": [{"text": "test2", "Y": "10.000000"}],
- "meeting_id": 1,
- "global_yes": True,
- "global_no": True,
- "global_abstain": True,
- "onehundred_percent_base": "Y",
- "content_object_id": "topic/1",
- },
- )
-
- def test_create_permissions_topic(self) -> None:
- self.create_topic(13, 1)
- self.base_permission_test(
- {},
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "pollmethod": "Y",
- "options": [{"text": "test2", "Y": "10.000000"}],
- "meeting_id": 1,
- "global_yes": True,
- "global_no": True,
- "global_abstain": True,
- "onehundred_percent_base": "Y",
- "content_object_id": "topic/1",
- },
- )
-
- def test_non_negative_max_votes_per_option(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "content_object_id": "assignment/1",
- "pollmethod": "Y",
- "options": [{"text": "test2", "Y": "10.000000"}],
- "meeting_id": 1,
- "global_yes": True,
- "global_no": True,
- "global_abstain": True,
- "onehundred_percent_base": "Y",
- "min_votes_amount": 5,
- "max_votes_amount": 10,
- "max_votes_per_option": -1,
- "backend": "long",
- },
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("poll/1")
-
- def test_max_votes_per_option_smaller_max_votes_amount(self) -> None:
- """Also asserts that default values are respected."""
- response = self.request(
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "pollmethod": "Y",
- "options": [{"text": "test2", "Y": "10.000000"}],
- "meeting_id": 1,
- "max_votes_per_option": 2,
- },
- )
- self.assert_status_code(response, 400)
- assert (
- response.json["message"]
- == "The maximum votes per option cannot be higher than the maximum amount of votes in total."
- )
- self.assert_model_not_exists("poll/1")
-
- def test_max_votes_amount_smaller_min(self) -> None:
- response = self.request(
- "poll.create",
- {
- "title": "test",
- "type": "analog",
- "pollmethod": "Y",
- "options": [{"text": "test2", "Y": "10.000000"}],
- "meeting_id": 1,
- "min_votes_amount": 5,
- "max_votes_amount": 2,
- "max_votes_per_option": 2,
- },
- )
- self.assert_status_code(response, 400)
- assert (
- response.json["message"]
- == "The minimum amount of votes cannot be higher than the maximum amount of votes."
- )
- self.assert_model_not_exists("poll/1")
-
- def test_create_poll_candidate_list(self) -> None:
- self.set_models({"user/3": {"username": "User3"}})
- response = self.request(
- "poll.create",
- {
- "title": "test",
- "type": Poll.TYPE_NAMED,
- "content_object_id": "assignment/1",
- "pollmethod": "YNA",
- "options": [{"poll_candidate_user_ids": [1, 3]}],
- "meeting_id": 1,
- "backend": "long",
- },
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists("assignment/1", {"poll_ids": [1]})
- self.assert_model_exists(
- "poll/1",
- {"title": "test", "option_ids": [1], "content_object_id": "assignment/1"},
- )
- self.assert_model_exists(
- "option/1", {"content_object_id": "poll_candidate_list/1", "poll_id": 1}
- )
- self.assert_model_exists(
- "poll_candidate_list/1",
- {"option_id": 1, "meeting_id": 1, "poll_candidate_ids": [1, 2]},
- )
- self.assert_model_exists(
- "poll_candidate/1",
- {"user_id": 1, "weight": 1, "poll_candidate_list_id": 1, "meeting_id": 1},
- )
- self.assert_model_exists(
- "poll_candidate/2",
- {"user_id": 3, "weight": 2, "poll_candidate_list_id": 1, "meeting_id": 1},
- )
- self.assert_model_exists(
- "meeting/1", {"poll_candidate_list_ids": [1], "poll_candidate_ids": [1, 2]}
- )
- self.assert_model_exists(
- "meeting/1",
- {
- "poll_ids": [1],
- "option_ids": [1, 2],
- "poll_candidate_ids": [1, 2],
- "poll_candidate_list_ids": [1],
- },
- )
-
- def test_create_poll_candidate_lists(self) -> None:
- self.set_models(
- {
- "user/2": {"username": "User2"},
- "user/3": {"username": "User3"},
- "user/4": {"username": "User4"},
- }
- )
- response = self.request(
- "poll.create",
- {
- "title": "test",
- "type": Poll.TYPE_PSEUDOANONYMOUS,
- "content_object_id": "assignment/1",
- "pollmethod": "YNA",
- "options": [
- {"poll_candidate_user_ids": [1, 3]},
- {"poll_candidate_user_ids": [2, 4]},
- ],
- "meeting_id": 1,
- "backend": "long",
- },
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1",
- {
- "title": "test",
- "option_ids": [1, 2],
- "content_object_id": "assignment/1",
- },
- )
- self.assert_model_exists(
- "option/1", {"content_object_id": "poll_candidate_list/1", "poll_id": 1}
- )
- self.assert_model_exists(
- "option/2", {"content_object_id": "poll_candidate_list/2", "poll_id": 1}
- )
- self.assert_model_exists(
- "poll_candidate_list/1",
- {"option_id": 1, "meeting_id": 1, "poll_candidate_ids": [1, 2]},
- )
- self.assert_model_exists(
- "poll_candidate_list/2",
- {"option_id": 2, "meeting_id": 1, "poll_candidate_ids": [3, 4]},
- )
-
- def test_with_anonymous_in_entitled_group_ids(self) -> None:
- self.set_anonymous()
- response = self.request(
- "poll.create",
- {
- "meeting_id": 1,
- "options": [{"text": "test"}],
- "pollmethod": "YNA",
- "title": "test",
- "type": Poll.TYPE_NAMED,
- "entitled_group_ids": [4],
- "content_object_id": "assignment/1",
- },
- )
- self.assert_status_code(response, 400)
- self.assertIn(
- "Anonymous group is not allowed in entitled_group_ids.",
- response.json["message"],
- )
-
- def test_live_voting_named_motion_poll(self) -> None:
- self.create_motion(1, 3)
- self.set_models({"motion_state/1": {"allow_create_poll": True}})
- response = self.request(
- "poll.create",
- {
- "title": "test_title_yaiyeighoh0Iraet3Ahc",
- "pollmethod": "YNA",
- "type": Poll.TYPE_NAMED,
- "content_object_id": "motion/3",
- "onehundred_percent_base": "YN",
- "meeting_id": 1,
- "options": [{"text": "test"}],
- "live_voting_enabled": True,
- },
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1", {"type": Poll.TYPE_NAMED, "live_voting_enabled": True}
- )
-
- def test_live_voting_named_assignment_poll(self) -> None:
- self.base_live_voting_assigment()
-
- def test_live_votng_named_assignment_poll_wrong_pollmethod(self) -> None:
- self.base_live_voting_assigment({"pollmethod": "YN"})
-
- def test_live_votng_named_assignment_poll_wrong_globalyes(self) -> None:
- self.base_live_voting_assigment({"global_yes": True})
-
- def test_live_votng_named_assignment_poll_wrong_max_votes(self) -> None:
- self.base_live_voting_assigment({"max_votes_amount": 2})
-
- def base_live_voting_assigment(
- self, error_dict: dict[str, typing.Any] | None = None
- ) -> None:
- self.create_assignment(3, 1)
-
- response = self.request(
- "poll.create",
- {
- "title": "test_title_yaiyeighoh0Iraet3Ahc",
- "pollmethod": "Y",
- "type": Poll.TYPE_NAMED,
- "content_object_id": "assignment/3",
- "onehundred_percent_base": "Y",
- "meeting_id": 1,
- "options": [{"text": "test"}],
- "live_voting_enabled": True,
- **(error_dict if error_dict else {}),
- },
- )
- if not error_dict:
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1", {"type": Poll.TYPE_NAMED, "live_voting_enabled": True}
- )
- else:
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("poll/1")
- assert (
- "live_voting_enabled only allowed for named motion polls and named Yes assignment polls."
- ) in response.json["message"]
-
- def test_live_voting_not_allowed_type_analog(self) -> None:
- self.base_live_voting_not_allowed(Poll.TYPE_ANALOG, True)
-
- def test_live_voting_not_allowed_type_pseudoanonymous(self) -> None:
- self.base_live_voting_not_allowed(Poll.TYPE_PSEUDOANONYMOUS, True)
-
- def test_live_voting_not_allowed_is_motion_poll_false(self) -> None:
- self.base_live_voting_not_allowed(Poll.TYPE_NAMED, False)
-
- def base_live_voting_not_allowed(
- self, poll_type: str, is_motion_poll: bool
- ) -> None:
- request_data = {
- "title": "test_title_yaiyeighoh0Iraet3Ahc",
- "pollmethod": "YNA",
- "type": poll_type,
- "content_object_id": "assignment/1",
- "onehundred_percent_base": "YN",
- "meeting_id": 1,
- "options": [{"text": "test"}],
- "live_voting_enabled": True,
- }
- if is_motion_poll:
- self.create_motion(1, 3)
- self.set_models({"motion_state/1": {"allow_create_poll": True}})
- request_data["content_object_id"] = "motion/3"
-
- response = self.request("poll.create", request_data)
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("poll/1")
- self.assertEqual(
- "live_voting_enabled only allowed for named motion polls and named Yes assignment polls.",
- response.json["message"],
- )
diff --git a/tests/system/action/poll/test_delete.py b/tests/system/action/poll/test_delete.py
deleted file mode 100644
index e0e25af65a..0000000000
--- a/tests/system/action/poll/test_delete.py
+++ /dev/null
@@ -1,178 +0,0 @@
-from unittest.mock import Mock, patch
-
-from openslides_backend.models.models import Poll
-from openslides_backend.permissions.permissions import Permissions
-from openslides_backend.services.database.interface import PartialModel
-from tests.system.util import CountDatastoreCalls, Profiler, performance
-
-from .base_poll_test import BasePollTestCase
-from .poll_test_mixin import PollTestMixin
-
-
-class PollDeleteTest(PollTestMixin, BasePollTestCase):
- def setUp(self) -> None:
- super().setUp()
- self.create_meeting()
- self.poll_base_data = {
- "title": "Poll 111",
- "pollmethod": "YNA",
- "type": Poll.TYPE_NAMED,
- "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_YN,
- "meeting_id": 1,
- "content_object_id": "motion/1",
- "state": Poll.STATE_STARTED,
- }
-
- def set_up_poll(self, poll_data: PartialModel = {}) -> None:
- self.set_models({"poll/111": self.poll_base_data | poll_data})
-
- @patch("openslides_backend.services.vote.adapter.VoteAdapter.clear")
- def test_delete_correct(self, clear: Mock) -> None:
- """
- Also ensures that the vote service is notified on success if the poll is started.
- """
- clear_called_on: list[int] = []
-
- def add_to_list(id_: int) -> None:
- clear_called_on.append(id_)
-
- clear.side_effect = add_to_list
- self.create_motion(1, 1)
- self.set_up_poll()
- response = self.request("poll.delete", {"id": 111})
- self.assert_status_code(response, 200)
- self.assert_model_not_exists("poll/111")
- self.assert_history_information("motion/1", ["Voting deleted"])
- assert clear_called_on == [111]
-
- def test_delete_wrong_id(self) -> None:
- self.create_motion(1, 1)
- self.set_up_poll()
- response = self.request("poll.delete", {"id": 110})
- self.assert_status_code(response, 400)
- self.assert_model_exists("poll/111")
-
- @patch("openslides_backend.services.vote.adapter.VoteAdapter.clear")
- def test_delete_correct_cascading(self, clear: Mock) -> None:
- """
- Also ensures that the vote service is not notified if the poll wasn't started anyway.
- """
- clear_called_on: list[int] = []
-
- def add_to_list(id_: int) -> None:
- clear_called_on.append(id_)
-
- clear.side_effect = add_to_list
- self.create_topic(1, 1)
- self.set_up_poll({"content_object_id": "topic/1", "state": Poll.STATE_CREATED})
- self.set_models(
- {
- "option/42": {"poll_id": 111, "meeting_id": 1},
- "projection/1": {
- "content_object_id": "poll/111",
- "current_projector_id": 1,
- "meeting_id": 1,
- },
- }
- )
- response = self.request("poll.delete", {"id": 111})
- self.assert_status_code(response, 200)
- self.assert_model_not_exists("poll/111")
- self.assert_model_not_exists("option/42")
- self.assert_model_not_exists("projection/1")
- self.assert_model_exists("projector/1", {"current_projection_ids": None})
- assert clear_called_on == []
-
- def test_delete_cascading_poll_candidate_list(self) -> None:
- self.create_meeting()
- self.create_topic(1, 1)
- self.set_up_poll({"content_object_id": "topic/1"})
- self.set_models(
- {
- "option/42": {
- "poll_id": 111,
- "meeting_id": 1,
- "content_object_id": "poll_candidate_list/12",
- },
- "poll_candidate_list/12": {
- "meeting_id": 1,
- "option_id": 42,
- },
- "poll_candidate/13": {
- "meeting_id": 1,
- "user_id": 1,
- "weight": 1,
- "poll_candidate_list_id": 12,
- },
- }
- )
- response = self.request("poll.delete", {"id": 111})
- self.assert_status_code(response, 200)
- self.assert_model_not_exists("poll/111")
- self.assert_model_not_exists("option/42")
- self.assert_model_not_exists("poll_candidate_list/12")
-
- @patch("openslides_backend.services.vote.adapter.VoteAdapter.clear")
- def test_delete_no_permissions(self, clear: Mock) -> None:
- """
- Also ensures that the vote service is not notified if the action failed.
- """
- clear_called_on: list[int] = []
-
- def add_to_list(id_: int) -> None:
- clear_called_on.append(id_)
-
- clear.side_effect = add_to_list
- self.create_topic(1, 1)
- self.set_up_poll({"content_object_id": "topic/1"})
- self.base_permission_test(
- {},
- "poll.delete",
- {"id": 111},
- )
-
- assert clear_called_on == []
-
- def test_delete_permissions(self) -> None:
- self.create_topic(1, 1)
- self.set_up_poll({"content_object_id": "topic/1"})
- self.base_permission_test(
- {},
- "poll.delete",
- {"id": 111},
- Permissions.Poll.CAN_MANAGE,
- )
-
- def test_delete_permissions_locked_meeting(self) -> None:
- self.create_topic(1, 1)
- self.set_up_poll({"content_object_id": "topic/1"})
- self.base_locked_out_superadmin_permission_test(
- {},
- "poll.delete",
- {"id": 111},
- )
-
- def test_delete_datastore_calls(self) -> None:
- self.prepare_users_and_poll(3)
-
- with CountDatastoreCalls() as counter:
- response = self.request("poll.delete", {"id": 1})
-
- self.assert_status_code(response, 200)
- self.assert_model_not_exists("poll/1")
- assert counter.calls == 16
-
- @performance
- def test_delete_performance(self) -> None:
- # TODO this needs a different idea
- user_ids = self.prepare_users_and_poll(1000)
- response = self.request("poll.stop", {"id": 1})
- self.assert_status_code(response, 200)
- self.datastore.reset(hard=True)
-
- with Profiler("test_delete_performance.prof"):
- response = self.request("poll.delete", {"id": 1})
-
- self.assert_status_code(response, 200)
- poll = self.get_model("poll/1")
- assert poll["voted_ids"] == user_ids
diff --git a/tests/system/action/poll/test_publish.py b/tests/system/action/poll/test_publish.py
deleted file mode 100644
index 25609ec6cf..0000000000
--- a/tests/system/action/poll/test_publish.py
+++ /dev/null
@@ -1,98 +0,0 @@
-from decimal import Decimal
-from typing import Any
-
-from openslides_backend.permissions.permissions import Permissions
-
-from .base_poll_test import BasePollTestCase
-
-
-class PollPublishActionTest(BasePollTestCase):
- def setUp(self) -> None:
- super().setUp()
- self.create_meeting()
- self.create_topic(1, 1)
- self.poll_data: dict[str, dict[str, Any]] = {
- "poll/1": {
- "type": "named",
- "pollmethod": "Y",
- "backend": "long",
- "state": "finished",
- "meeting_id": 1,
- "content_object_id": "topic/1",
- "title": "Poll 1",
- "onehundred_percent_base": "YNA",
- },
- }
-
- def test_publish_correct(self) -> None:
- self.set_models(self.poll_data)
- response = self.request("poll.publish", {"id": 1})
- self.assert_status_code(response, 200)
- poll = self.get_model("poll/1")
- assert poll.get("state") == "published"
- self.assert_history_information("topic/1", None)
-
- def test_publish_motion(self) -> None:
- self.create_motion(1, 2)
- self.poll_data["poll/1"]["content_object_id"] = "motion/2"
- self.set_models(self.poll_data)
- response = self.request("poll.publish", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_history_information("motion/2", ["Voting published"])
-
- def test_publish_assignment(self) -> None:
- self.create_assignment(1, 1)
- self.poll_data["poll/1"]["content_object_id"] = "assignment/1"
- self.set_models(self.poll_data)
- response = self.request("poll.publish", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_history_information("assignment/1", ["Ballot published"])
-
- def test_publish_wrong_state(self) -> None:
- self.poll_data["poll/1"]["state"] = "created"
- self.set_models(self.poll_data)
- response = self.request("poll.publish", {"id": 1})
- self.assert_status_code(response, 400)
- poll = self.get_model("poll/1")
- assert poll.get("state") == "created"
- self.assertEqual(
- "Cannot publish poll 1, because it is not in state finished or started.",
- response.json["message"],
- )
-
- def test_publish_started(self) -> None:
- self.create_motion(1, 2)
- self.poll_data["poll/1"]["content_object_id"] = "motion/2"
- self.poll_data["poll/1"]["state"] = "started"
- self.set_models(self.poll_data)
- self.vote_service.start(1)
- response = self.request("poll.publish", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1",
- {
- "votescast": Decimal("0.000000"),
- "votesinvalid": Decimal("0.000000"),
- "votesvalid": Decimal("0.000000"),
- "entitled_users_at_stop": [],
- },
- )
- self.assert_history_information("motion/2", ["Voting stopped/published"])
-
- def test_publish_no_permissions(self) -> None:
- self.base_permission_test(self.poll_data, "poll.publish", {"id": 1})
-
- def test_publish_permissions(self) -> None:
- self.base_permission_test(
- self.poll_data,
- "poll.publish",
- {"id": 1},
- Permissions.Poll.CAN_MANAGE,
- )
-
- def test_publish_permissions_locked_meeting(self) -> None:
- self.base_locked_out_superadmin_permission_test(
- self.poll_data,
- "poll.publish",
- {"id": 1},
- )
diff --git a/tests/system/action/poll/test_reset.py b/tests/system/action/poll/test_reset.py
deleted file mode 100644
index eef890cf4c..0000000000
--- a/tests/system/action/poll/test_reset.py
+++ /dev/null
@@ -1,182 +0,0 @@
-from decimal import Decimal
-from typing import Any
-
-from psycopg.types.json import Jsonb
-
-from openslides_backend.models.models import Poll
-from openslides_backend.permissions.permissions import Permissions
-from tests.system.util import CountDatastoreCalls, Profiler, performance
-
-from .base_poll_test import BasePollTestCase
-from .poll_test_mixin import PollTestMixin
-
-
-class PollResetActionTest(PollTestMixin, BasePollTestCase):
- def setUp(self) -> None:
- super().setUp()
- self.create_meeting()
- self.test_models: dict[str, dict[str, Any]] = {
- "poll/1": {
- "title": "Poll 1",
- "pollmethod": "YNA",
- "type": Poll.TYPE_NAMED,
- "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_YN,
- "state": Poll.STATE_STARTED,
- "global_option_id": 2,
- "meeting_id": 1,
- "content_object_id": "topic/1",
- },
- "option/1": {"poll_id": 1, "meeting_id": 1},
- "option/2": {"meeting_id": 1},
- "vote/1": {"option_id": 1, "meeting_id": 1, "user_token": "abs"},
- "vote/2": {"option_id": 1, "meeting_id": 1, "user_token": "def"},
- "vote/3": {"option_id": 2, "meeting_id": 1, "user_token": "ghi"},
- }
-
- def test_reset_correct(self) -> None:
- self.create_topic(1, 1)
- self.test_models["poll/1"].update(
- {
- "votesvalid": "3.000000",
- "votesinvalid": "1.000000",
- "votescast": "5.000000",
- "entitled_users_at_stop": Jsonb([{"user_id": 1, "voted": True}]),
- }
- )
- self.set_models(self.test_models)
-
- response = self.request("poll.reset", {"id": 1})
- self.assert_status_code(response, 200)
-
- # check if the state has been changed to 1 (Created).
- poll = self.get_model("poll/1")
- assert poll.get("state") == "created"
-
- # check if not is_pseudoanonymized
- assert poll.get("is_pseudoanonymized") is False
-
- # check if voted_ids is cleared
- assert poll.get("voted_ids") is None
-
- # check if auto generated fields are cleared
- assert poll.get("entitled_users_at_stop") is None
- assert poll.get("votesvalid") is None
- assert poll.get("votesinvalid") is None
- assert poll.get("votescast") is None
-
- # check if the votes are deleted
- self.assert_model_not_exists("vote/1")
- self.assert_model_not_exists("vote/2")
- self.assert_model_not_exists("vote/3")
-
- # check if the option.vote_ids fields are cleared
- self.assert_model_exists(
- "option/1",
- {
- "vote_ids": None,
- "yes": Decimal("0.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/2",
- {
- "vote_ids": None,
- "yes": Decimal("0.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
-
- # test history
- self.assert_history_information("topic/1", None)
-
- def test_reset_motion(self) -> None:
- self.create_motion(1, 1)
- self.test_models["poll/1"]["content_object_id"] = "motion/1"
- self.set_models(self.test_models)
- response = self.request("poll.reset", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_history_information("motion/1", ["Voting reset"])
-
- def test_reset_assignment(self) -> None:
- self.create_assignment(1, 1)
- self.test_models["poll/1"]["content_object_id"] = "assignment/1"
- self.set_models(self.test_models)
- response = self.request("poll.reset", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_history_information("assignment/1", ["Ballot reset"])
-
- def test_reset_no_permissions(self) -> None:
- self.create_topic(1, 1)
- self.base_permission_test(self.test_models, "poll.reset", {"id": 1})
-
- def test_reset_permissions(self) -> None:
- self.create_topic(1, 1)
- self.base_permission_test(
- self.test_models,
- "poll.reset",
- {"id": 1},
- Permissions.Poll.CAN_MANAGE,
- )
-
- def test_reset_permissions_locked_meeting(self) -> None:
- self.create_topic(1, 1)
- self.base_locked_out_superadmin_permission_test(
- self.test_models,
- "poll.reset",
- {"id": 1},
- )
-
- def test_reset_not_allowed_to_vote_again(self) -> None:
- self.create_topic(1, 1)
- self.test_models["poll/1"].update(
- {
- "pollmethod": "Y",
- "max_votes_per_option": 1,
- "backend": "long",
- "onehundred_percent_base": "Y",
- }
- )
- self.test_models["group/1"] = {"poll_ids": [1]}
- self.set_models(self.test_models)
- self.set_user_groups(1, [1])
- self.set_models({"meeting/1": {"present_user_ids": [1]}})
- self.vote_service.start(1)
- response = self.vote_service.vote({"id": 1, "value": {"1": 1}})
- self.assert_status_code(response, 200)
- response = self.request("poll.reset", {"id": 1})
- self.assert_status_code(response, 200)
- response = self.request("poll.start", {"id": 1})
- self.assert_status_code(response, 200)
- response = self.vote_service.vote({"id": 1, "value": {"1": 1}})
- self.assert_status_code(response, 200)
-
- def test_reset_datastore_calls(self) -> None:
- self.prepare_users_and_poll(3)
-
- with CountDatastoreCalls() as counter:
- response = self.request("poll.reset", {"id": 1})
-
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1", {"voted_ids": None, "state": Poll.STATE_CREATED}
- )
- assert counter.calls == 14
-
- @performance
- def test_reset_performance(self) -> None:
- # TODO this needs a different idea
- self.prepare_users_and_poll(100)
- response = self.request("poll.stop", {"id": 1})
- self.assert_status_code(response, 200)
- self.datastore.reset(hard=True)
-
- with Profiler("test_reset_performance.prof"):
- response = self.request("poll.reset", {"id": 1})
-
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1", {"voted_ids": [], "state": Poll.STATE_CREATED}
- )
diff --git a/tests/system/action/poll/test_set_state.py b/tests/system/action/poll/test_set_state.py
deleted file mode 100644
index 4d0a4d74f0..0000000000
--- a/tests/system/action/poll/test_set_state.py
+++ /dev/null
@@ -1,32 +0,0 @@
-from decimal import Decimal
-
-from .base_poll_test import BasePollTestCase
-
-
-class PollSetState(BasePollTestCase):
- def test_set_state(self) -> None:
- self.create_meeting(110)
- self.create_assignment(1, 110)
- self.set_models(
- {
- "poll/65": {
- "title": "Title 65",
- "type": "analog",
- "state": "created",
- "pollmethod": "YNA",
- "meeting_id": 110,
- "content_object_id": "assignment/1",
- },
- "option/57": {
- "yes": Decimal("0.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- "meeting_id": 110,
- "poll_id": 65,
- },
- }
- )
- response = self.request("poll.set_state", {"id": 65, "state": "finished"})
- self.assert_status_code(response, 200)
- poll = self.get_model("poll/65")
- assert poll.get("state") == "finished"
diff --git a/tests/system/action/poll/test_start.py b/tests/system/action/poll/test_start.py
deleted file mode 100644
index 34cdc97ba8..0000000000
--- a/tests/system/action/poll/test_start.py
+++ /dev/null
@@ -1,207 +0,0 @@
-from decimal import Decimal
-from typing import Any
-
-from openslides_backend.models.models import Poll
-
-from .base_poll_test import BasePollTestCase
-
-
-class VotePollBaseTestClass(BasePollTestCase):
- def setUp(self) -> None:
- super().setUp()
- self.create_meeting(meeting_data={"poll_couple_countdown": True})
- self.create_assignment(1, 1)
- self.set_user_groups(1, [1])
- self.set_models(
- {
- "meeting/1": {"present_user_ids": [1], "poll_countdown_id": 11},
- "projector_countdown/11": {
- "title": "Countdown 11",
- "default_time": 60,
- "running": False,
- "countdown_time": 60,
- "meeting_id": 1,
- },
- "group/1": {"poll_ids": [1]},
- "option/1": {"meeting_id": 1, "poll_id": 1},
- "option/2": {"meeting_id": 1, "poll_id": 1},
- "poll/1": {
- "content_object_id": "assignment/1",
- "title": "test_title_04k0y4TwPLpJKaSvIGm1",
- "state": Poll.STATE_CREATED,
- "meeting_id": 1,
- "votesinvalid": Decimal("0.000000"),
- "votesvalid": Decimal("0.000000"),
- "votescast": Decimal("0.000000"),
- "backend": "fast",
- **self.get_poll_data(),
- "onehundred_percent_base": "YNA",
- },
- }
- )
-
- def get_poll_data(self) -> dict[str, Any]:
- # has to be implemented by subclasses
- raise NotImplementedError()
-
-
-class VotePollAnalogYNA(VotePollBaseTestClass):
- def get_poll_data(self) -> dict[str, Any]:
- return {
- "pollmethod": "YNA",
- "type": Poll.TYPE_ANALOG,
- }
-
- def test_start_analog_poll(self) -> None:
- response = self.request("poll.start", {"id": 1})
- self.assert_status_code(response, 400)
- assert (
- "Analog polls cannot be started. Please use poll.update instead to give votes."
- in response.json["message"]
- )
- poll = self.get_model("poll/1")
- self.assertEqual(poll.get("state"), Poll.STATE_CREATED)
-
-
-class VotePollNamedYNA(VotePollBaseTestClass):
- def get_poll_data(self) -> dict[str, Any]:
- return {
- "pollmethod": "YNA",
- "type": Poll.TYPE_NAMED,
- }
-
- def test_start_poll(self) -> None:
- response = self.request("poll.start", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1",
- {
- "state": Poll.STATE_STARTED,
- "votesvalid": Decimal("0.000000"),
- "votesinvalid": Decimal("0.000000"),
- "votescast": Decimal("0.000000"),
- },
- )
- self.assert_model_not_exists("vote/1")
- # test history
- self.assert_history_information("assignment/1", ["Ballot started"])
- # test that votes can be given
- response = self.vote_service.vote({"id": 1, "value": {"1": "Y"}})
- self.assert_status_code(response, 200)
-
- def test_start_motion_poll(self) -> None:
- self.create_motion(1, 1)
- self.set_models({"poll/1": {"content_object_id": "motion/1"}})
- response = self.request("poll.start", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_history_information("motion/1", ["Voting started"])
-
-
-class VotePollNamedY(VotePollBaseTestClass):
- def get_poll_data(self) -> dict[str, Any]:
- return {
- "pollmethod": "Y",
- "type": Poll.TYPE_NAMED,
- }
-
- def test_start_poll(self) -> None:
- response = self.request("poll.start", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1",
- {
- "state": Poll.STATE_STARTED,
- "votesvalid": Decimal("0.000000"),
- "votesinvalid": Decimal("0.000000"),
- "votescast": Decimal("0.000000"),
- },
- )
- self.assert_model_not_exists("vote/1")
-
-
-class VotePollNamedN(VotePollBaseTestClass):
- def get_poll_data(self) -> dict[str, Any]:
- return {
- "pollmethod": "N",
- "type": Poll.TYPE_NAMED,
- }
-
- def test_start_poll(self) -> None:
- response = self.request("poll.start", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1",
- {
- "state": Poll.STATE_STARTED,
- "votesvalid": Decimal("0.000000"),
- "votesinvalid": Decimal("0.000000"),
- "votescast": Decimal("0.000000"),
- },
- )
- self.assert_model_not_exists("vote/1")
-
-
-class VotePollPseudoanonymousYNA(VotePollBaseTestClass):
- def get_poll_data(self) -> dict[str, Any]:
- return {
- "pollmethod": "YNA",
- "type": Poll.TYPE_PSEUDOANONYMOUS,
- }
-
- def test_start_poll(self) -> None:
- response = self.request("poll.start", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1",
- {
- "state": Poll.STATE_STARTED,
- "votesvalid": Decimal("0.000000"),
- "votesinvalid": Decimal("0.000000"),
- "votescast": Decimal("0.000000"),
- },
- )
- self.assert_model_not_exists("vote/1")
-
-
-class VotePollPseudoanonymousY(VotePollBaseTestClass):
- def get_poll_data(self) -> dict[str, Any]:
- return {
- "pollmethod": "Y",
- "type": Poll.TYPE_PSEUDOANONYMOUS,
- }
-
- def test_start_poll(self) -> None:
- response = self.request("poll.start", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1",
- {
- "state": Poll.STATE_STARTED,
- "votesvalid": Decimal("0.000000"),
- "votesinvalid": Decimal("0.000000"),
- "votescast": Decimal("0.000000"),
- },
- )
- self.assert_model_not_exists("vote/1")
-
-
-class VotePollPseudoAnonymousN(VotePollBaseTestClass):
- def get_poll_data(self) -> dict[str, Any]:
- return {
- "pollmethod": "N",
- "type": Poll.TYPE_PSEUDOANONYMOUS,
- }
-
- def test_start_poll(self) -> None:
- response = self.request("poll.start", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1",
- {
- "state": Poll.STATE_STARTED,
- "votesvalid": Decimal("0.000000"),
- "votesinvalid": Decimal("0.000000"),
- "votescast": Decimal("0.000000"),
- },
- )
- self.assert_model_not_exists("vote/1")
diff --git a/tests/system/action/poll/test_stop.py b/tests/system/action/poll/test_stop.py
deleted file mode 100644
index c9f98f6a59..0000000000
--- a/tests/system/action/poll/test_stop.py
+++ /dev/null
@@ -1,370 +0,0 @@
-from decimal import Decimal
-from typing import Any
-from unittest.mock import Mock, patch
-
-from openslides_backend.models.models import Poll
-from openslides_backend.permissions.permissions import Permissions
-from tests.system.util import CountDatastoreCalls, Profiler, performance
-
-from .base_poll_test import BasePollTestCase
-from .poll_test_mixin import PollTestMixin
-
-
-class PollStopActionTest(PollTestMixin, BasePollTestCase):
- def setUp(self) -> None:
- super().setUp()
- self.poll_data: dict[str, Any] = {
- "type": Poll.TYPE_NAMED,
- "pollmethod": "Y",
- "backend": "fast",
- "content_object_id": "topic/1",
- "state": Poll.STATE_STARTED,
- "meeting_id": 1,
- "title": "Poll 1",
- "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_Y,
- }
-
- @patch("openslides_backend.services.vote.adapter.VoteAdapter.clear")
- def test_stop_correct(self, clear: Mock) -> None:
- clear_called_on: list[int] = []
-
- def add_to_list(id_: int) -> None:
- clear_called_on.append(id_)
-
- clear.side_effect = add_to_list
- self.create_meeting()
- self.create_motion(1, 1)
- user1 = self.create_user_for_meeting(1)
- user2 = self.create_user_for_meeting(1)
- user3 = self.create_user_for_meeting(1)
- self.set_models(
- {
- "poll/1": {
- **self.poll_data,
- "content_object_id": "motion/1",
- "pollmethod": "YN",
- "entitled_group_ids": [1],
- },
- "group/1": {"poll_ids": [1]},
- "option/1": {"meeting_id": 1, "poll_id": 1},
- "meeting/1": {
- "users_enable_vote_weight": True,
- "poll_couple_countdown": True,
- "poll_countdown_id": 1,
- "users_enable_vote_delegations": True,
- "present_user_ids": [user1, user2],
- },
- "projector_countdown/1": {
- "title": "Countdown 1",
- "running": True,
- "default_time": 60,
- "countdown_time": 30.0,
- "meeting_id": 1,
- },
- f"user/{user1}": {"default_vote_weight": Decimal("2.000000")},
- f"user/{user2}": {"default_vote_weight": Decimal("3.000000")},
- "meeting_user/1": {"vote_weight": Decimal("2.600000")},
- "meeting_user/2": {"vote_weight": Decimal("3.600000")},
- "meeting_user/3": {
- "vote_weight": Decimal("4.600000"),
- "vote_delegated_to_id": 1,
- },
- }
- )
- self.start_poll(1)
- self.login(user1)
- response = self.vote_service.vote({"id": 1, "value": {"1": "Y"}})
- self.assert_status_code(response, 200)
- response = self.vote_service.vote(
- {"id": 1, "user_id": user3, "value": {"1": "N"}}
- )
- self.assert_status_code(response, 200)
-
- self.login(1)
- response = self.request("poll.stop", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "projector_countdown/1",
- {
- "running": False,
- "countdown_time": 60,
- },
- )
- self.assert_model_exists(
- "poll/1",
- {
- "voted_ids": [2, 4],
- "state": Poll.STATE_FINISHED,
- "votescast": Decimal("2.000000"),
- "votesinvalid": Decimal("0.000000"),
- "votesvalid": Decimal("7.200000"),
- "entitled_users_at_stop": [
- {
- "voted": True,
- "present": True,
- "user_id": user1,
- "vote_delegated_to_user_id": None,
- },
- {
- "voted": False,
- "present": True,
- "user_id": user2,
- "vote_delegated_to_user_id": None,
- },
- {
- "voted": True,
- "present": False,
- "user_id": user3,
- "vote_delegated_to_user_id": user1,
- },
- ],
- },
- )
- # test history
- self.assert_history_information("motion/1", ["Voting stopped"])
- assert clear_called_on == [1]
-
- def test_stop_assignment_poll(self) -> None:
- self.create_meeting()
- self.create_assignment(1, 1)
- self.set_models(
- {
- "poll/1": {
- **self.poll_data,
- "content_object_id": "assignment/1",
- "pollmethod": "YN",
- },
- }
- )
- self.start_poll(1)
- response = self.request("poll.stop", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_history_information("assignment/1", ["Ballot stopped"])
-
- def test_stop_entitled_users_at_stop_user_only_once(self) -> None:
- self.create_meeting()
- self.create_motion(1, 1)
- self.create_user("User2")
- self.set_user_groups(2, [2, 3])
- self.set_models(
- {
- "poll/1": {
- **self.poll_data,
- "content_object_id": "motion/1",
- "entitled_group_ids": [3, 4],
- },
- "meeting/1": {"present_user_ids": [2]},
- "group/2": {"poll_ids": [1]},
- "group/3": {"poll_ids": [1]},
- }
- )
- self.start_poll(1)
- response = self.request("poll.stop", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1",
- {
- "entitled_users_at_stop": [
- {
- "voted": False,
- "present": True,
- "user_id": 2,
- "vote_delegated_to_user_id": None,
- },
- ]
- },
- )
-
- def test_stop_entitled_users_not_present(self) -> None:
- self.create_meeting()
- self.create_motion(1, 1)
- user2 = self.create_user_for_meeting(1)
- user3 = self.create_user_for_meeting(1)
- self.set_user_groups(user2, [1])
- self.set_user_groups(user3, [3])
- self.set_models(
- {
- "poll/1": {**self.poll_data, "content_object_id": "motion/1"},
- "group/1": {"poll_ids": [1]},
- }
- )
- self.start_poll(1)
- response = self.request("poll.stop", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1",
- {
- "entitled_users_at_stop": [
- {
- "voted": False,
- "present": False,
- "user_id": 2,
- "vote_delegated_to_user_id": None,
- },
- ]
- },
- )
-
- def test_stop_entitled_users_with_delegations(self) -> None:
- self.create_meeting(meeting_data={"users_enable_vote_delegations": True})
- self.create_motion(1, 1)
- user2 = self.create_user_for_meeting(1)
- user3 = self.create_user_for_meeting(1)
- self.set_user_groups(user2, [1])
- self.set_user_groups(user3, [3])
- self.set_models(
- {
- "poll/1": {**self.poll_data, "content_object_id": "motion/1"},
- "group/1": {"poll_ids": [1]},
- "meeting_user/1": {"vote_delegated_to_id": 2},
- }
- )
- self.start_poll(1)
- response = self.request("poll.stop", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1",
- {
- "entitled_users_at_stop": [
- {
- "voted": False,
- "present": False,
- "user_id": 2,
- "vote_delegated_to_user_id": 3,
- },
- ]
- },
- )
-
- def test_stop_entitled_users_with_delegations_turned_off(self) -> None:
- self.create_meeting(meeting_data={"users_enable_vote_delegations": False})
- self.create_motion(1, 1)
- user2 = self.create_user_for_meeting(1)
- user3 = self.create_user_for_meeting(1)
- self.set_user_groups(user2, [1])
- self.set_user_groups(user3, [3])
- self.set_models(
- {
- "poll/1": {**self.poll_data, "content_object_id": "motion/1"},
- "group/1": {"poll_ids": [1]},
- "meeting_user/1": {"vote_delegated_to_id": 2},
- }
- )
- self.start_poll(1)
- response = self.request("poll.stop", {"id": 1})
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1",
- {
- "entitled_users_at_stop": [
- {
- "voted": False,
- "present": False,
- "user_id": 2,
- "vote_delegated_to_user_id": None,
- },
- ]
- },
- )
-
- def test_stop_published(self) -> None:
- self.create_meeting()
- self.create_motion(1, 1)
- self.set_models(
- {
- "poll/1": {
- **self.poll_data,
- "content_object_id": "motion/1",
- "state": Poll.STATE_PUBLISHED,
- },
- }
- )
- response = self.request("poll.stop", {"id": 1})
- self.assert_status_code(response, 400)
- poll = self.get_model("poll/1")
- assert poll.get("state") == Poll.STATE_PUBLISHED
- assert (
- "Cannot stop poll 1, because it is not in state started."
- in response.json["message"]
- )
-
- def test_stop_created(self) -> None:
- self.create_meeting()
- self.create_motion(1, 1)
- self.set_models(
- {
- "poll/1": {
- **self.poll_data,
- "content_object_id": "motion/1",
- "state": Poll.STATE_CREATED,
- }
- }
- )
- response = self.request("poll.stop", {"id": 1})
- self.assert_status_code(response, 400)
- poll = self.get_model("poll/1")
- assert poll.get("state") == Poll.STATE_CREATED
- assert (
- "Cannot stop poll 1, because it is not in state started."
- in response.json["message"]
- )
-
- @patch("openslides_backend.services.vote.adapter.VoteAdapter.clear")
- def test_stop_no_permissions(self, clear: Mock) -> None:
- clear_called_on: list[int] = []
-
- def add_to_list(id_: int) -> None:
- clear_called_on.append(id_)
-
- clear.side_effect = add_to_list
- self.create_meeting()
- self.create_topic(1, 1)
- self.set_models({"poll/1": self.poll_data})
- self.start_poll(1)
- self.base_permission_test({}, "poll.stop", {"id": 1})
-
- assert clear_called_on == []
-
- def test_stop_permissions(self) -> None:
- self.create_meeting()
- self.create_topic(1, 1)
- self.set_models({"poll/1": self.poll_data})
- self.start_poll(1)
- self.base_permission_test(
- {},
- "poll.stop",
- {"id": 1},
- Permissions.Poll.CAN_MANAGE,
- )
-
- def test_stop_permissions_locked_meeting(self) -> None:
- self.create_meeting()
- self.create_topic(1, 1)
- self.set_models({"poll/1": self.poll_data})
- self.start_poll(1)
- self.base_locked_out_superadmin_permission_test(
- {},
- "poll.stop",
- {"id": 1},
- )
-
- def test_stop_datastore_calls(self) -> None:
- user_ids = self.prepare_users_and_poll(3)
-
- with CountDatastoreCalls() as counter:
- response = self.request("poll.stop", {"id": 1})
-
- self.assert_status_code(response, 200)
- self.assert_model_exists("poll/1", {"voted_ids": user_ids})
- assert counter.calls == 63
-
- @performance
- def test_stop_performance(self) -> None:
- user_ids = self.prepare_users_and_poll(3)
-
- with Profiler("test_stop_performance.prof"):
- response = self.request("poll.stop", {"id": 1})
-
- self.assert_status_code(response, 200)
- poll = self.get_model("poll/1")
- assert poll["voted_ids"] == user_ids
diff --git a/tests/system/action/poll/test_update.py b/tests/system/action/poll/test_update.py
deleted file mode 100644
index f3c6166111..0000000000
--- a/tests/system/action/poll/test_update.py
+++ /dev/null
@@ -1,697 +0,0 @@
-from decimal import Decimal
-from typing import Any
-
-from psycopg.types.json import Jsonb
-
-from openslides_backend.models.models import Poll
-from openslides_backend.permissions.permissions import Permissions
-
-from .base_poll_test import BasePollTestCase
-
-
-class UpdatePollTestCase(BasePollTestCase):
- def setUp(self) -> None:
- super().setUp()
- self.entitled_users_at_stop_data: list[dict[str, Any]] = [
- {
- "voted": True,
- "present": True,
- "user_id": 2,
- "vote_delegated_to_user_id": None,
- },
- {
- "voted": True,
- "present": False,
- "user_id": 3,
- "vote_delegated_to_user_id": 2,
- },
- ]
-
- self.create_meeting()
- self.create_assignment(1, 1, {"open_posts": 1})
- self.set_models(
- {
- "meeting/1": {"present_user_ids": [1]},
- "group/1": {"poll_ids": [1], "meeting_user_ids": [11]},
- "poll/1": {
- "content_object_id": "assignment/1",
- "title": "test_title_beeFaihuNae1vej2ai8m",
- "pollmethod": "Y",
- "type": Poll.TYPE_NAMED,
- "onehundred_percent_base": "Y",
- "state": Poll.STATE_CREATED,
- "meeting_id": 1,
- "min_votes_amount": 1,
- "max_votes_amount": 1,
- "max_votes_per_option": 1,
- },
- "option/1": {"meeting_id": 1, "poll_id": 1},
- "option/2": {"meeting_id": 1, "poll_id": 1},
- "meeting_user/11": {"user_id": 1, "meeting_id": 1},
- }
- )
-
- def test_catch_not_allowed(self) -> None:
- self.update_model("poll/1", {"state": "started"})
- response = self.request(
- "poll.update",
- {
- "id": 1,
- "pollmethod": "Y",
- "type": "analog",
- "min_votes_amount": 1,
- "max_votes_amount": 1,
- "max_votes_per_option": 1,
- "global_yes": False,
- "global_no": True,
- "global_abstain": True,
- },
- )
- self.assert_status_code(response, 400)
- assert ("data must not contain {'type'} properties") in response.json["message"]
-
- def test_optional_state_created(self) -> None:
- response = self.request(
- "poll.update",
- {
- "id": 1,
- "pollmethod": "Y",
- "min_votes_amount": 1,
- "max_votes_amount": 1,
- "max_votes_per_option": 1,
- "global_yes": False,
- "global_no": True,
- "global_abstain": True,
- },
- )
- self.assert_status_code(response, 200)
- poll = self.get_model("poll/1")
- assert poll.get("pollmethod") == "Y"
- assert poll.get("min_votes_amount") == 1
- assert poll.get("max_votes_amount") == 1
- assert poll.get("max_votes_per_option") == 1
- assert poll.get("global_yes") is False
- assert poll.get("global_no") is True
- assert poll.get("global_abstain") is True
- self.assert_history_information("assignment/1", ["Ballot updated"])
-
- def test_not_allowed_for_analog(self) -> None:
- self.update_model("poll/1", {"type": "analog"})
- response = self.request("poll.update", {"id": 1, "entitled_group_ids": []})
- self.assert_status_code(response, 400)
- assert (
- "Following options are not allowed in this state and type: "
- "entitled_group_ids"
- ) in response.json["message"]
-
- def test_not_allowed_for_non_analog(self) -> None:
- response = self.request(
- "poll.update",
- {
- "id": 1,
- "votesvalid": "10.000000",
- "votesinvalid": "11.000000",
- "votescast": "3.000000",
- "publish_immediately": True,
- },
- )
- self.assert_status_code(response, 400)
- assert (
- "Following options are not allowed in this state and type: "
- "votesvalid, votesinvalid, votescast, publish_immediately"
- ) in response.json["message"]
-
- def test_update_title(self) -> None:
- response = self.request(
- "poll.update",
- {"title": "test_title_Aishohh1ohd0aiSut7gi", "id": 1},
- )
- self.assert_status_code(response, 200)
- poll = self.get_model("poll/1")
- self.assertEqual(poll.get("title"), "test_title_Aishohh1ohd0aiSut7gi")
-
- def test_prevent_updating_content_object(self) -> None:
- self.create_assignment(2, 1, {"open_posts": 1})
- response = self.request(
- "poll.update",
- {"content_object_id": "assignment/2", "id": 1},
- )
- self.assert_status_code(response, 400)
- poll = self.get_model("poll/1")
- self.assertEqual(poll.get("content_object_id"), "assignment/1") # unchanged
-
- def test_update_pollmethod(self) -> None:
- response = self.request(
- "poll.update",
- {"pollmethod": "YNA", "id": 1},
- )
- self.assert_status_code(response, 200)
- poll = self.get_model("poll/1")
- self.assertEqual(poll.get("pollmethod"), "YNA")
- self.assertEqual(poll.get("onehundred_percent_base"), "Y")
-
- def test_update_backend(self) -> None:
- response = self.request(
- "poll.update",
- {"backend": "long", "id": 1},
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists("poll/1", {"backend": "long"})
-
- def test_update_backend_not_allowed(self) -> None:
- self.set_models({"poll/1": {"state": Poll.STATE_FINISHED}})
- response = self.request(
- "poll.update",
- {"backend": "long", "id": 1},
- )
- self.assert_status_code(response, 400)
- assert (
- "Following options are not allowed in this state and type: backend"
- in response.json["message"]
- )
-
- def test_update_invalid_pollmethod(self) -> None:
- response = self.request(
- "poll.update",
- {"pollmethod": "invalid"},
- )
- self.assert_status_code(response, 400)
- poll = self.get_model("poll/1")
- self.assertEqual(poll.get("pollmethod"), "Y")
-
- def test_update_groups_to_empty(self) -> None:
- response = self.request("poll.update", {"entitled_group_ids": [], "id": 1})
- self.assert_status_code(response, 200)
- poll = self.get_model("poll/1")
- assert poll.get("entitled_group_ids") is None
-
- def test_update_groups(self) -> None:
- response = self.request(
- "poll.update",
- {"entitled_group_ids": [2], "id": 1},
- )
- self.assert_status_code(response, 200)
- poll = self.get_model("poll/1")
- self.assertEqual(poll.get("entitled_group_ids"), [2])
-
- def test_update_groups_with_anonymous(self) -> None:
- group_id = self.set_anonymous()
- response = self.request(
- "poll.update",
- {"entitled_group_ids": [group_id], "id": 1},
- )
- self.assert_status_code(response, 400)
- self.assertIn(
- "Anonymous group is not allowed in entitled_group_ids.",
- response.json["message"],
- )
-
- def test_update_title_started(self) -> None:
- self.update_model("poll/1", {"state": Poll.STATE_STARTED})
- response = self.request(
- "poll.update",
- {"title": "test_title_Oophah8EaLaequu3toh8", "id": 1},
- )
- self.assert_status_code(response, 200)
- poll = self.get_model("poll/1")
- self.assertEqual(poll.get("title"), "test_title_Oophah8EaLaequu3toh8")
-
- def test_update_wrong_state(self) -> None:
- self.update_model("poll/1", {"state": Poll.STATE_STARTED})
- response = self.request(
- "poll.update",
- {"type": Poll.TYPE_NAMED, "id": 1},
- )
- self.assert_status_code(response, 400)
- poll = self.get_model("poll/1")
- self.assertEqual(poll.get("type"), Poll.TYPE_NAMED)
-
- def test_update_max_votes_per_option(self) -> None:
- response = self.request(
- "poll.update",
- {"max_votes_per_option": 5, "max_votes_amount": 5, "id": 1},
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists("poll/1", {"max_votes_per_option": 5})
-
- def test_max_votes_per_option_smaller_max_votes_amount(self) -> None:
- response = self.request(
- "poll.update",
- {"max_votes_per_option": 5, "max_votes_amount": 1, "id": 1},
- )
- self.assert_status_code(response, 400)
- assert (
- response.json["message"]
- == "The maximum votes per option cannot be higher than the maximum amount of votes in total."
- )
- self.assert_model_exists("poll/1", {"max_votes_per_option": 1})
-
- def test_max_votes_amount_smaller_min(self) -> None:
- response = self.request(
- "poll.update",
- {"min_votes_amount": 5, "max_votes_amount": 1, "id": 1},
- )
- self.assert_status_code(response, 400)
- assert (
- response.json["message"]
- == "The minimum amount of votes cannot be higher than the maximum amount of votes."
- )
- self.assert_model_exists("poll/1", {"min_votes_amount": 1})
-
- def test_update_negative_fields(self) -> None:
- for field in ("max_votes_per_option", "max_votes_amount", "min_votes_amount"):
- response = self.request(
- "poll.update",
- {field: -3, "id": 1},
- )
- self.assert_status_code(response, 400)
- poll = self.get_model("poll/1")
- self.assertEqual(poll.get(field), 1)
-
- def test_update_100_percent_base(self) -> None:
- response = self.request(
- "poll.update",
- {"onehundred_percent_base": "cast", "id": 1},
- )
- self.assert_status_code(response, 200)
- poll = self.get_model("poll/1")
- self.assertEqual(poll.get("onehundred_percent_base"), "cast")
-
- def test_update_wrong_100_percent_base(self) -> None:
- response = self.request(
- "poll.update",
- {"onehundred_percent_base": "invalid", "id": 1},
- )
- self.assert_status_code(response, 400)
- poll = self.get_model("poll/1")
- self.assertEqual(poll.get("onehundred_percent_base"), "Y")
-
- def test_update_multiple_fields(self) -> None:
- response = self.request(
- "poll.update",
- {
- "id": 1,
- "title": "test_title_ees6Tho8ahheen4cieja",
- "pollmethod": "Y",
- "global_yes": True,
- "global_no": True,
- "global_abstain": False,
- "max_votes_per_option": 2,
- "max_votes_amount": 2,
- },
- )
- self.assert_status_code(response, 200)
- poll = self.get_model("poll/1")
- self.assertEqual(poll.get("title"), "test_title_ees6Tho8ahheen4cieja")
- self.assertEqual(poll.get("pollmethod"), "Y")
- self.assertTrue(poll.get("global_yes"))
- self.assertTrue(poll.get("global_no"))
- self.assertFalse(poll.get("global_abstain"))
- self.assertEqual(poll.get("max_votes_per_option"), 2)
-
- def test_update_max_votes_per_option_state_not_created(self) -> None:
- self.update_model("poll/1", {"state": Poll.STATE_STARTED})
- response = self.request(
- "poll.update",
- {"max_votes_per_option": 3, "id": 1},
- )
- self.assert_status_code(response, 400)
- poll = self.get_model("poll/1")
- self.assertEqual(poll.get("max_votes_per_option"), 1)
-
- def test_update_100_percent_base_state_not_created(self) -> None:
- self.update_model("poll/1", {"state": Poll.STATE_STARTED})
- response = self.request(
- "poll.update",
- {"onehundred_percent_base": "cast", "id": 1},
- )
- self.assert_status_code(response, 200)
- poll = self.get_model("poll/1")
- self.assertEqual(poll.get("onehundred_percent_base"), "cast")
-
- def test_update_wrong_100_percent_base_state_not_created(self) -> None:
- self.update_model("poll/1", {"state": Poll.STATE_STARTED, "pollmethod": "YN"})
- response = self.request(
- "poll.update",
- {"onehundred_percent_base": "YNA", "id": 1},
- )
- self.assert_status_code(response, 400)
- assert (
- "This onehundred_percent_base not allowed in this pollmethod"
- in response.json["message"]
- )
- poll = self.get_model("poll/1")
- self.assertEqual(poll.get("onehundred_percent_base"), "Y")
-
- def test_update_wrong_100_percent_base_entitled_and_analog(self) -> None:
- self.update_model(
- "poll/1",
- {"state": Poll.STATE_STARTED, "pollmethod": "YN", "type": Poll.TYPE_ANALOG},
- )
- response = self.request(
- "poll.update",
- {"onehundred_percent_base": "entitled", "id": 1},
- )
- self.assert_status_code(response, 400)
- assert (
- "onehundred_percent_base: value entitled is not allowed for analog."
- in response.json["message"]
- )
-
- def test_state_change(self) -> None:
- self.update_model("poll/1", {"type": Poll.TYPE_ANALOG})
- response = self.request("poll.update", {"id": 1, "votescast": "1.000000"})
- self.assert_status_code(response, 200)
- poll = self.get_model("poll/1")
- assert poll.get("state") == Poll.STATE_FINISHED
-
- def test_state_change_2_published(self) -> None:
- self.update_model("poll/1", {"type": Poll.TYPE_ANALOG})
- response = self.request(
- "poll.update",
- {"id": 1, "votescast": "1.000000", "publish_immediately": True},
- )
- self.assert_status_code(response, 200)
- poll = self.get_model("poll/1")
- assert poll.get("state") == Poll.STATE_PUBLISHED
- assert "publish_immediately" not in poll
-
- def test_default_vote_values(self) -> None:
- self.update_model("poll/1", {"type": Poll.TYPE_ANALOG})
- response = self.request(
- "poll.update",
- {"id": 1, "votescast": "1.000000"},
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1",
- {
- "votescast": Decimal("1.000000"),
- "votesvalid": Decimal("-2.000000"),
- "votesinvalid": Decimal("-2.000000"),
- },
- )
-
- def test_motion_history_information(self) -> None:
- self.create_motion(1, 2)
- self.set_models({"poll/1": {"content_object_id": "motion/2"}})
- response = self.request(
- "poll.update",
- {"id": 1, "title": "test"},
- )
- self.assert_status_code(response, 200)
- self.assert_history_information("motion/2", ["Voting updated"])
-
- def test_update_no_permissions(self) -> None:
- self.base_permission_test(
- {},
- "poll.update",
- {"title": "test_title_Aishohh1ohd0aiSut7gi", "id": 1},
- )
-
- def test_update_permissions(self) -> None:
- self.base_permission_test(
- {},
- "poll.update",
- {"title": "test_title_Aishohh1ohd0aiSut7gi", "id": 1},
- Permissions.Assignment.CAN_MANAGE_POLLS,
- )
-
- def test_update_wrong_permissions(self) -> None:
- self.base_permission_test(
- {},
- "poll.update",
- {"title": "test_title_Aishohh1ohd0aiSut7gi", "id": 1},
- Permissions.Assignment.CAN_MANAGE,
- fail=True,
- )
-
- def test_update_permissions_locked_meeting(self) -> None:
- self.base_locked_out_superadmin_permission_test(
- {},
- "poll.update",
- {"title": "test_title_Aishohh1ohd0aiSut7gi", "id": 1},
- )
-
- def test_update_entitled_users_at_stop_error(self) -> None:
- response = self.request(
- "poll.update",
- {"entitled_users_at_stop": self.entitled_users_at_stop_data, "id": 1},
- internal=False,
- )
- self.assert_status_code(response, 400)
- assert (
- "data must not contain {'entitled_users_at_stop'} properties"
- in response.json["message"]
- )
-
- def test_update_entitled_users_at_stop_fields_changed_error(self) -> None:
- self.set_models(
- {
- "poll/1": {
- "entitled_users_at_stop": Jsonb(self.entitled_users_at_stop_data)
- }
- }
- )
- response = self.request(
- "poll.update",
- {
- "entitled_users_at_stop": [
- {**self.entitled_users_at_stop_data[0], "voted": False},
- self.entitled_users_at_stop_data[1],
- ],
- "id": 1,
- },
- internal=True,
- )
- self.assert_status_code(response, 400)
- assert (
- "Can not change essential 'entitled_users_at_stop' data via poll.update"
- in response.json["message"]
- )
-
- def test_update_entitled_users_at_stop_initial_set_attempt_error(self) -> None:
- response = self.request(
- "poll.update",
- {"entitled_users_at_stop": self.entitled_users_at_stop_data, "id": 1},
- internal=True,
- )
- self.assert_status_code(response, 400)
- assert (
- "Can not set 'entitled_users_at_stop' via poll.update"
- in response.json["message"]
- )
-
- def test_update_entitled_users_at_stop_list_shortened_error(self) -> None:
- self.set_models(
- {
- "poll/1": {
- "entitled_users_at_stop": Jsonb(self.entitled_users_at_stop_data)
- }
- }
- )
- response = self.request(
- "poll.update",
- {"entitled_users_at_stop": [self.entitled_users_at_stop_data[1]], "id": 1},
- internal=True,
- )
- self.assert_status_code(response, 400)
- assert (
- "Can not change essential 'entitled_users_at_stop' data via poll.update"
- in response.json["message"]
- )
-
- def test_update_entitled_users_at_stop_list_lengthened_error(self) -> None:
- self.set_models(
- {
- "poll/1": {
- "entitled_users_at_stop": Jsonb(self.entitled_users_at_stop_data)
- }
- }
- )
- response = self.request(
- "poll.update",
- {
- "entitled_users_at_stop": [
- *self.entitled_users_at_stop_data,
- {"voted": True, "present": True, "user_id": 5},
- ],
- "id": 1,
- },
- internal=True,
- )
- self.assert_status_code(response, 400)
- assert (
- "Can not change essential 'entitled_users_at_stop' data via poll.update"
- in response.json["message"]
- )
-
- def test_update_entitled_users_at_stop_wrong_format_error(self) -> None:
- self.set_models(
- {
- "poll/1": {
- "entitled_users_at_stop": Jsonb(self.entitled_users_at_stop_data)
- }
- }
- )
- response = self.request(
- "poll.update",
- {"entitled_users_at_stop": {"this": "shouldn't be a dict"}, "id": 1},
- internal=True,
- )
- self.assert_status_code(response, 400)
- assert (
- "'entitled_users_at_stop' has the wrong format" in response.json["message"]
- )
-
- def test_update_entitled_users_at_stop_wrong_format_error_2(self) -> None:
- self.set_models(
- {
- "poll/1": {
- "entitled_users_at_stop": Jsonb(self.entitled_users_at_stop_data)
- }
- }
- )
- response = self.request(
- "poll.update",
- {"entitled_users_at_stop": ["this shouldn't be a string"], "id": 1},
- internal=True,
- )
- self.assert_status_code(response, 400)
- assert (
- "'entitled_users_at_stop' has the wrong format" in response.json["message"]
- )
-
- def test_update_entitled_users_at_stop_wrong_format_error_3(self) -> None:
- self.set_models(
- {
- "poll/1": {
- "entitled_users_at_stop": Jsonb(self.entitled_users_at_stop_data)
- }
- }
- )
- response = self.request(
- "poll.update",
- {
- "entitled_users_at_stop": [{"this": "is still the wrong format"}],
- "id": 1,
- },
- internal=True,
- )
- self.assert_status_code(response, 400)
- assert (
- "'entitled_users_at_stop' has the wrong format" in response.json["message"]
- )
-
- def test_update_entitled_users_at_stop_nothing_changed(self) -> None:
- self.set_models(
- {
- "poll/1": {
- "entitled_users_at_stop": Jsonb(self.entitled_users_at_stop_data)
- }
- }
- )
- response = self.request(
- "poll.update",
- {"entitled_users_at_stop": self.entitled_users_at_stop_data, "id": 1},
- internal=True,
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1",
- {"entitled_users_at_stop": self.entitled_users_at_stop_data},
- )
-
- def test_update_entitled_users_at_stop_fields_changed_success(self) -> None:
- self.set_models(
- {
- "poll/1": {
- "entitled_users_at_stop": Jsonb(self.entitled_users_at_stop_data)
- }
- }
- )
- response = self.request(
- "poll.update",
- {
- "entitled_users_at_stop": [
- {
- **self.entitled_users_at_stop_data[0],
- "delegation_user_merged_into_id": 9,
- "user_merged_into_id": 10,
- },
- self.entitled_users_at_stop_data[1],
- ],
- "id": 1,
- },
- internal=True,
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll/1",
- {
- "entitled_users_at_stop": [
- {
- **self.entitled_users_at_stop_data[0],
- "delegation_user_merged_into_id": 9,
- "user_merged_into_id": 10,
- },
- self.entitled_users_at_stop_data[1],
- ]
- },
- )
-
- def test_live_voting_named_motion_poll(self) -> None:
- self.create_motion(1, 3)
- self.set_models({"motion_state/1": {"allow_create_poll": True}})
- self.update_model(
- "poll/1", {"type": Poll.TYPE_NAMED, "content_object_id": "motion/3"}
- )
-
- response = self.request("poll.update", {"id": 1, "live_voting_enabled": True})
- self.assert_status_code(response, 200)
- self.assert_model_exists("poll/1", {"live_voting_enabled": True})
-
- def test_live_voting_named_assignment_poll(self) -> None:
- self.update_model("poll/1", {"type": Poll.TYPE_NAMED})
-
- response = self.request("poll.update", {"id": 1, "live_voting_enabled": True})
- self.assert_status_code(response, 200)
- self.assert_model_exists("poll/1", {"live_voting_enabled": True})
-
- def test_live_voting_not_allowed_type_analog(self) -> None:
- self.base_live_voting_not_allowed(Poll.TYPE_ANALOG, True)
-
- def test_live_voting_not_allowed_type_pseudoanonymous(self) -> None:
- self.base_live_voting_not_allowed(Poll.TYPE_PSEUDOANONYMOUS, True)
-
- def test_live_voting_not_allowed_assignment_poll_pollmethod(self) -> None:
- self.base_live_voting_not_allowed(Poll.TYPE_NAMED, False, {"pollmethod": "YN"})
-
- def test_live_voting_not_allowed_assignment_poll_global_yes(self) -> None:
- self.base_live_voting_not_allowed(Poll.TYPE_NAMED, False, {"global_yes": True})
-
- def test_live_voting_not_allowed_assignment_poll_max_votes_amount(self) -> None:
- self.base_live_voting_not_allowed(
- Poll.TYPE_NAMED, False, {"max_votes_amount": 2}
- )
-
- def base_live_voting_not_allowed(
- self,
- poll_type: str,
- is_motion_poll: bool,
- poll_changes: dict[str, Any] | None = None,
- ) -> None:
- if is_motion_poll:
- self.create_motion(1, 3)
- self.set_models({"motion_state/1": {"allow_create_poll": True}})
- self.update_model("poll/1", {"content_object_id": "motion/3"})
- elif poll_changes:
- self.update_model("poll/1", poll_changes)
- self.update_model("poll/1", {"type": poll_type})
-
- response = self.request("poll.update", {"id": 1, "live_voting_enabled": True})
- self.assert_status_code(response, 400)
- self.assert_model_exists("poll/1", {"live_voting_enabled": False})
- assert (
- "live_voting_enabled only allowed for named motion polls and named Yes assignment polls."
- ) in response.json["message"]
diff --git a/tests/system/action/poll/test_vote.py b/tests/system/action/poll/test_vote.py
index 6fb3834dec..d344bfc55f 100644
--- a/tests/system/action/poll/test_vote.py
+++ b/tests/system/action/poll/test_vote.py
@@ -1,11 +1,8 @@
-from decimal import Decimal
from typing import Any
import requests
import simplejson as json
-from openslides_backend.models.models import Poll
-from openslides_backend.services.database.interface import PartialModel
from tests.system.util import convert_to_test_response
from tests.util import Response
@@ -47,2118 +44,3 @@ def anonymous_vote(self, payload: dict[str, Any], id: int = 1) -> Response:
},
)
return convert_to_test_response(response)
-
-
-class PollVoteTest(BaseVoteTestCase):
- def setUp(self) -> None:
- super().setUp()
- self.create_meeting(113)
- self.create_motion(113, 1)
-
- def create_poll_with_3_options(self, poll_data: PartialModel = {}) -> None:
- self.set_models(
- {
- "meeting/113": {"present_user_ids": [1]},
- "group/113": {"meeting_user_ids": [11], "poll_ids": [1]},
- "option/11": {"meeting_id": 113, "poll_id": 1},
- "option/12": {"meeting_id": 113, "poll_id": 1},
- "option/13": {"meeting_id": 113, "poll_id": 1},
- "poll/1": {
- "content_object_id": "motion/1",
- "title": "my test poll",
- "pollmethod": "YN",
- "meeting_id": 113,
- "state": Poll.STATE_CREATED,
- "backend": "fast",
- "type": "named",
- "onehundred_percent_base": "YNA",
- **poll_data,
- },
- "meeting_user/11": {"user_id": 1, "meeting_id": 113},
- }
- )
-
- def test_vote_correct_pollmethod_Y(self) -> None:
- user_id = self.create_user("test2")
- self.set_models(
- {
- "group/113": {"meeting_user_ids": [11, 12], "poll_ids": [1]},
- "option/11": {"meeting_id": 113, "poll_id": 1},
- "meeting_user/11": {
- "meeting_id": 113,
- "user_id": 1,
- },
- "meeting_user/12": {
- "meeting_id": 113,
- "user_id": user_id,
- "vote_weight": Decimal("2.000000"),
- },
- "poll/1": {
- "content_object_id": "motion/1",
- "title": "my test poll",
- "pollmethod": "Y",
- "meeting_id": 113,
- "state": Poll.STATE_CREATED,
- "min_votes_amount": 1,
- "max_votes_amount": 10,
- "max_votes_per_option": 1,
- "backend": "fast",
- "type": "named",
- "onehundred_percent_base": "YNA",
- },
- "meeting/113": {
- "users_enable_vote_weight": True,
- "present_user_ids": [1, 2],
- },
- }
- )
- response = self.request(
- "poll.vote", {"id": 1, "value": {"11": 1}}, stop_poll_after_vote=False
- )
- self.assert_status_code(response, 200)
- self.login(user_id)
- response = self.request(
- "poll.vote", {"id": 1, "value": {"11": 1}}, start_poll_before_vote=False
- )
- self.assert_status_code(response, 200)
- for i in range(1, 3):
- vote = self.assert_model_exists(
- f"vote/{i}", {"value": "Y", "option_id": 11, "meeting_id": 113}
- )
- user_id = vote.get("user_id", 0)
- assert user_id == vote.get("delegated_user_id")
- self.assert_model_exists(
- f"user/{user_id}",
- {
- "poll_voted_ids": [1],
- "delegated_vote_ids": [i],
- "vote_ids": [vote["id"]],
- },
- )
- assert vote.get("weight") == Decimal(f"{user_id}.000000")
- self.assert_model_exists(
- "option/11",
- {
- "vote_ids": [1, 2],
- "yes": Decimal("3.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
-
- def test_value_check(self) -> None:
- self.create_poll_with_3_options(
- {
- "min_votes_amount": 1,
- "max_votes_amount": 10,
- }
- )
- response = self.request(
- "poll.vote",
- {
- "id": 1,
- "user_id": 1,
- "value": {"11": "Y", "12": "N", "13": "A"},
- },
- )
- self.assert_status_code(response, 400)
- assert (
- "Data for option 13 does not fit the poll method."
- in response.json["message"]
- )
-
- def test_vote_correct_pollmethod_YN(self) -> None:
- self.create_poll_with_3_options(
- {
- "min_votes_amount": 1,
- "max_votes_amount": 10,
- "max_votes_per_option": 1,
- }
- )
- response = self.request(
- "poll.vote",
- {
- "id": 1,
- "user_id": 1,
- "value": {"11": "Y", "12": "N"},
- },
- )
- self.assert_status_code(response, 200)
- vote = self.assert_model_exists(
- "vote/1",
- {
- "value": "Y",
- "option_id": 11,
- "weight": Decimal("1.000000"),
- "meeting_id": 113,
- "user_id": 1,
- "delegated_user_id": 1,
- },
- )
- user_token = vote.get("user_token")
- vote = self.assert_model_exists(
- "vote/2",
- {
- "value": "N",
- "option_id": 12,
- "weight": Decimal("1.000000"),
- "meeting_id": 113,
- "user_id": 1,
- "delegated_user_id": 1,
- },
- )
- assert vote.get("user_token") == user_token
- self.assert_model_exists(
- "option/11",
- {
- "vote_ids": [1],
- "yes": Decimal("1.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/12",
- {
- "vote_ids": [2],
- "yes": Decimal("0.000000"),
- "no": Decimal("1.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
-
- def test_vote_wrong_votes_total(self) -> None:
- self.create_poll_with_3_options(
- {
- "pollmethod": "Y",
- "min_votes_amount": 1,
- "max_votes_amount": 1,
- "max_votes_per_option": 1,
- "onehundred_percent_base": "Y",
- }
- )
- response = self.request(
- "poll.vote",
- {
- "id": 1,
- "user_id": 1,
- "value": {"11": 1, "12": 1},
- },
- )
- self.assert_status_code(response, 400)
- assert (
- "The sum of your answers has to be between 1 and 1"
- in response.json["message"]
- )
- self.assert_model_not_exists("vote/1")
-
- def test_vote_pollmethod_Y_wrong_value(self) -> None:
- self.create_poll_with_3_options({"pollmethod": "Y"})
- response = self.request(
- "poll.vote",
- {
- "id": 1,
- "value": {"11": "Y"},
- },
- )
- self.assert_status_code(response, 400)
- assert "Your vote has a wrong format" in response.json["message"]
- self.assert_model_not_exists("vote/1")
-
- def test_vote_no_votes_total_check_by_YNA(self) -> None:
- self.create_poll_with_3_options(
- {
- "pollmethod": "YNA",
- "min_votes_amount": 1,
- "max_votes_amount": 2,
- "max_votes_per_option": 1,
- }
- )
- response = self.request(
- "poll.vote",
- {
- "id": 1,
- "user_id": 1,
- "value": {"11": "Y", "12": "A"},
- },
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists("vote/1")
-
- def test_vote_no_votes_total_check_by_YNA_max_votes_error(self) -> None:
- self.create_poll_with_3_options(
- {
- "pollmethod": "YNA",
- "min_votes_amount": 1,
- "max_votes_amount": 1,
- "max_votes_per_option": 1,
- }
- )
- response = self.request(
- "poll.vote",
- {
- "id": 1,
- "user_id": 1,
- "value": {"11": "Y", "12": "A"},
- },
- )
- self.assert_status_code(response, 400)
- assert "You have to select between 1 and 1 options" in response.json["message"]
-
- def test_vote_no_votes_total_check_by_YN(self) -> None:
- self.create_poll_with_3_options(
- {"max_votes_amount": 10, "max_votes_per_option": 1}
- )
- response = self.request(
- "poll.vote",
- {
- "id": 1,
- "user_id": 1,
- "value": {"11": "Y", "12": "N"},
- },
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists("vote/1")
-
- def test_vote_wrong_votes_total_min_case(self) -> None:
- self.create_poll_with_3_options(
- {
- "pollmethod": "Y",
- "min_votes_amount": 2,
- "max_votes_amount": 2,
- "max_votes_per_option": 1,
- }
- )
- response = self.request(
- "poll.vote",
- {
- "id": 1,
- "user_id": 1,
- "value": {"11": 1},
- },
- )
- self.assert_status_code(response, 400)
- assert (
- "The sum of your answers has to be between 2 and 2"
- in response.json["message"]
- )
- self.assert_model_not_exists("vote/1")
-
- def test_vote_global(self) -> None:
- self.create_user("test2")
- self.set_models(
- {
- "meeting/113": {"present_user_ids": [1, 2]},
- "group/113": {"meeting_user_ids": [11, 12], "poll_ids": [1]},
- "option/11": {"meeting_id": 113, "used_as_global_option_in_poll_id": 1},
- "meeting_user/11": {"user_id": 1, "meeting_id": 113},
- "meeting_user/12": {"user_id": 2, "meeting_id": 113},
- "poll/1": {
- "content_object_id": "motion/1",
- "title": "my test poll",
- "global_option_id": 11,
- "global_no": True,
- "global_yes": False,
- "global_abstain": False,
- "meeting_id": 113,
- "state": Poll.STATE_CREATED,
- "pollmethod": "YNA",
- "backend": "fast",
- "type": "named",
- "onehundred_percent_base": "Y",
- },
- }
- )
- response = self.request(
- "poll.vote",
- {"id": 1, "user_id": 1, "value": "N"},
- stop_poll_after_vote=False,
- )
- self.assert_status_code(response, 200)
- response = self.request(
- "poll.vote",
- {"id": 1, "user_id": 2, "value": "Y"},
- start_poll_before_vote=False,
- )
- self.assert_status_code(response, 400)
-
- self.assert_model_exists(
- "vote/1",
- {
- "value": "N",
- "option_id": 11,
- "weight": Decimal("1.000000"),
- "meeting_id": 113,
- "user_id": 1,
- },
- )
- self.assert_model_exists(
- "option/11",
- {
- "vote_ids": [1],
- "yes": Decimal("0.000000"),
- "no": Decimal("1.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "user/1",
- {
- "poll_voted_ids": [1],
- "delegated_vote_ids": [1],
- "vote_ids": [1],
- },
- )
- self.assert_model_not_exists("vote/2")
-
- def test_vote_schema_problems(self) -> None:
- self.set_models(
- {
- "meeting/113": {"present_user_ids": [1]},
- "group/113": {"meeting_user_ids": [11], "poll_ids": [1]},
- "poll/1": {
- "content_object_id": "motion/1",
- "title": "my test poll",
- "meeting_id": 113,
- "pollmethod": "YNA",
- "state": Poll.STATE_CREATED,
- "backend": "fast",
- "type": "named",
- "onehundred_percent_base": "YNA",
- },
- "meeting_user/11": {"user_id": 1, "meeting_id": 113},
- }
- )
- response = self.request("poll.vote", {"id": 1, "user_id": 1, "value": "X"})
- self.assert_status_code(response, 400)
- assert "Global vote X is not enabled" in response.json["message"]
-
- def test_vote_invalid_vote_value(self) -> None:
- self.set_models(
- {
- "meeting/113": {"present_user_ids": [1]},
- "group/113": {"meeting_user_ids": [11], "poll_ids": [1]},
- "option/11": {"meeting_id": 113, "poll_id": 1},
- "poll/1": {
- "content_object_id": "motion/1",
- "option_ids": [11],
- "pollmethod": "YNA",
- "meeting_id": 113,
- "state": Poll.STATE_CREATED,
- "meeting_id": 113,
- "backend": "fast",
- "type": "named",
- "title": "Poll 1",
- "onehundred_percent_base": "YNA",
- },
- "meeting_user/11": {"user_id": 1, "meeting_id": 113},
- }
- )
- response = self.request(
- "poll.vote",
- {
- "id": 1,
- "user_id": 1,
- "value": {"11": "X"},
- },
- )
- self.assert_status_code(response, 400)
- assert (
- "Data for option 11 does not fit the poll method."
- in response.json["message"]
- )
-
- def test_vote_not_started_in_service(self) -> None:
- self.set_models(
- {
- "meeting/113": {"present_user_ids": [1]},
- "group/113": {"meeting_user_ids": [11], "poll_ids": [1]},
- "poll/1": {
- "content_object_id": "motion/1",
- "type": "named",
- "meeting_id": 113,
- "pollmethod": "YNA",
- "global_yes": True,
- "state": Poll.STATE_CREATED,
- "backend": "fast",
- "type": "named",
- "title": "Poll 1",
- "onehundred_percent_base": "YNA",
- },
- "meeting_user/11": {"user_id": 1, "meeting_id": 113},
- }
- )
- response = self.request(
- "poll.vote",
- {"id": 1, "value": "Y"},
- start_poll_before_vote=False,
- stop_poll_after_vote=False,
- )
- self.assert_status_code(response, 400)
- assert "Poll does not exist" in response.json["message"]
-
- def test_vote_option_not_in_poll(self) -> None:
- self.create_poll_with_3_options(
- {"pollmethod": "YNA", "onehundred_percent_base": "Y"}
- )
- response = self.request(
- "poll.vote",
- {
- "id": 1,
- "user_id": 1,
- "value": {"113": "Y"},
- },
- )
- self.assert_status_code(response, 400)
- assert "Option_id 113 does not belong to the poll" in response.json["message"]
-
- def test_double_vote(self) -> None:
- self.create_user("test2")
- self.set_models(
- {
- "meeting/113": {"present_user_ids": [1]},
- "group/113": {"meeting_user_ids": [11, 12], "poll_ids": [1]},
- "option/11": {"meeting_id": 113, "used_as_global_option_in_poll_id": 1},
- "meeting_user/11": {"user_id": 1, "meeting_id": 113},
- "meeting_user/12": {"user_id": 2, "meeting_id": 113},
- "poll/1": {
- "content_object_id": "motion/1",
- "title": "my test poll",
- "global_option_id": 11,
- "global_no": True,
- "global_yes": False,
- "global_abstain": False,
- "meeting_id": 113,
- "pollmethod": "YN",
- "state": Poll.STATE_CREATED,
- "backend": "fast",
- "type": "named",
- "onehundred_percent_base": "Y",
- },
- }
- )
- response = self.request(
- "poll.vote",
- {"id": 1, "user_id": 1, "value": "N"},
- stop_poll_after_vote=False,
- )
- self.assert_status_code(response, 200)
- response = self.request(
- "poll.vote",
- {"id": 1, "user_id": 1, "value": "N"},
- start_poll_before_vote=False,
- )
- self.assert_status_code(response, 400)
- assert "Not the first vote" in response.json["message"]
- self.assert_model_exists(
- "vote/1",
- {
- "value": "N",
- "option_id": 11,
- "weight": Decimal("1.000000"),
- "meeting_id": 113,
- "user_id": 1,
- "delegated_user_id": 1,
- },
- )
- self.assert_model_exists("option/11", {"vote_ids": [1]})
- self.assert_model_exists(
- "user/1",
- {"poll_voted_ids": [1], "vote_ids": [1], "delegated_vote_ids": [1]},
- )
-
- def test_check_user_in_entitled_group(self) -> None:
- self.set_models(
- {
- "meeting/113": {"present_user_ids": [1]},
- "group/113": {"meeting_user_ids": [11]},
- "option/11": {"meeting_id": 113, "used_as_global_option_in_poll_id": 1},
- "meeting_user/11": {"user_id": 1, "meeting_id": 113},
- "poll/1": {
- "content_object_id": "motion/1",
- "pollmethod": "YNA",
- "global_option_id": 11,
- "global_no": True,
- "global_yes": False,
- "global_abstain": False,
- "meeting_id": 113,
- "state": Poll.STATE_CREATED,
- "backend": "fast",
- "type": "named",
- "title": "Poll 1",
- "onehundred_percent_base": "YNA",
- },
- }
- )
- response = self.request("poll.vote", {"id": 1, "user_id": 1, "value": "N"})
- self.assert_status_code(response, 400)
- assert "User 1 is not allowed to vote" in response.json["message"]
-
- def test_check_user_present_in_meeting(self) -> None:
- self.set_models(
- {
- "group/113": {"meeting_user_ids": [11], "poll_ids": [1]},
- "meeting_user/11": {"user_id": 1, "meeting_id": 113},
- "option/11": {"meeting_id": 113, "used_as_global_option_in_poll_id": 1},
- "poll/1": {
- "content_object_id": "motion/1",
- "title": "my test poll",
- "global_option_id": 11,
- "global_no": True,
- "global_yes": False,
- "global_abstain": False,
- "meeting_id": 113,
- "state": Poll.STATE_CREATED,
- "backend": "fast",
- "type": "named",
- "pollmethod": "YNA",
- "onehundred_percent_base": "Y",
- },
- }
- )
- response = self.request("poll.vote", {"id": 1, "value": "N"})
- self.assert_status_code(response, 400)
- assert "You have to be present in meeting 113" in response.json["message"]
-
- def test_check_str_validation(self) -> None:
- self.set_models(
- {
- "meeting/113": {"present_user_ids": [1]},
- "group/113": {"meeting_user_ids": [11], "poll_ids": [1]},
- "poll/1": {
- "content_object_id": "motion/1",
- "title": "my test poll",
- "type": "named",
- "meeting_id": 113,
- "pollmethod": "Y",
- "state": Poll.STATE_CREATED,
- "backend": "fast",
- "onehundred_percent_base": "Y",
- },
- "meeting_user/11": {"user_id": 1, "meeting_id": 113},
- }
- )
- response = self.request("poll.vote", {"id": 1, "user_id": 1, "value": "X"})
- self.assert_status_code(response, 400)
- assert "Global vote X is not enabled" in response.json["message"]
-
- def test_default_vote_weight(self) -> None:
- self.set_models(
- {
- "group/113": {"meeting_user_ids": [11], "poll_ids": [1]},
- "option/11": {"meeting_id": 113, "poll_id": 1},
- "user/1": {"default_vote_weight": Decimal("3.000000")},
- "meeting_user/11": {"user_id": 1, "meeting_id": 113},
- "poll/1": {
- "content_object_id": "motion/1",
- "title": "my test poll",
- "pollmethod": "Y",
- "meeting_id": 113,
- "state": Poll.STATE_CREATED,
- "max_votes_per_option": 1,
- "backend": "fast",
- "type": "named",
- "onehundred_percent_base": "Y",
- },
- "meeting/113": {
- "users_enable_vote_weight": True,
- "present_user_ids": [1],
- },
- }
- )
- response = self.request(
- "poll.vote", {"id": 1, "user_id": 1, "value": {"11": 1}}
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "vote/1",
- {
- "value": "Y",
- "option_id": 11,
- "weight": Decimal("3.000000"),
- "meeting_id": 113,
- "user_id": 1,
- },
- )
- self.assert_model_exists(
- "option/11",
- {
- "vote_ids": [1],
- "yes": Decimal("3.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "user/1",
- {"poll_voted_ids": [1], "delegated_vote_ids": [1], "vote_ids": [1]},
- )
-
- def test_vote_weight_not_enabled(self) -> None:
- self.set_models(
- {
- "group/113": {"meeting_user_ids": [11], "poll_ids": [1]},
- "option/11": {"meeting_id": 113, "poll_id": 1},
- "user/1": {"default_vote_weight": Decimal("3.000000")},
- "meeting_user/11": {
- "meeting_id": 113,
- "user_id": 1,
- "vote_weight": Decimal("4.200000"),
- },
- "poll/1": {
- "content_object_id": "motion/1",
- "title": "my test poll",
- "pollmethod": "Y",
- "meeting_id": 113,
- "state": Poll.STATE_CREATED,
- "max_votes_per_option": 1,
- "backend": "fast",
- "type": "named",
- "onehundred_percent_base": "Y",
- },
- "meeting/113": {
- "users_enable_vote_weight": False,
- "present_user_ids": [1],
- },
- }
- )
- response = self.request(
- "poll.vote", {"id": 1, "user_id": 1, "value": {"11": 1}}
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "vote/1",
- {
- "value": "Y",
- "option_id": 11,
- "weight": Decimal("1.000000"),
- "meeting_id": 113,
- "user_id": 1,
- },
- )
- self.assert_model_exists(
- "option/11",
- {
- "vote_ids": [1],
- "yes": Decimal("1.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "user/1",
- {"poll_voted_ids": [1], "delegated_vote_ids": [1], "vote_ids": [1]},
- )
-
-
-class VotePollBaseTestClass(BaseVoteTestCase):
- def setUp(self) -> None:
- super().setUp()
- self.create_meeting(113)
- self.create_assignment(1, 113, {"open_posts": 1})
- self.create_poll()
- self.set_models(
- {
- "meeting/113": {"present_user_ids": [1]},
- "group/113": {"meeting_user_ids": [11], "poll_ids": [1]},
- "option/1": {
- "meeting_id": 113,
- "poll_id": 1,
- "yes": Decimal("0.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- "option/2": {
- "meeting_id": 113,
- "poll_id": 1,
- "yes": Decimal("0.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- "meeting_user/11": {"user_id": 1, "meeting_id": 113},
- "option/11": {"meeting_id": 113},
- "poll/1": {"global_option_id": 11},
- }
- )
-
- def create_poll(self) -> None:
- # has to be implemented by subclasses
- raise NotImplementedError()
-
- def start_poll(self) -> None:
- self.update_model("poll/1", {"state": Poll.STATE_CREATED})
-
- def add_option(self) -> None:
- self.set_models(
- {
- "option/3": {"meeting_id": 113, "poll_id": 1},
- }
- )
-
-
-class VotePollNamedYNA(VotePollBaseTestClass):
- def create_poll(self) -> None:
- self.create_model(
- "poll/1",
- {
- "content_object_id": "assignment/1",
- "title": "test_title_OkHAIvOSIcpFnCxbaL6v",
- "pollmethod": "YNA",
- "type": Poll.TYPE_NAMED,
- "state": Poll.STATE_CREATED,
- "meeting_id": 113,
- "votescast": Decimal("0.000000"),
- "votesvalid": Decimal("0.000000"),
- "votesinvalid": Decimal("0.000000"),
- "min_votes_amount": 1,
- "max_votes_amount": 10,
- "max_votes_per_option": 1,
- "onehundred_percent_base": "YNA",
- },
- )
-
- def test_vote(self) -> None:
- self.add_option()
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": "Y", "2": "N", "3": "A"}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 200)
- self.assert_model_count("vote", 113, 3)
- self.assert_model_exists(
- "poll/1",
- {
- "votesvalid": Decimal("1.000000"),
- "votesinvalid": Decimal("0.000000"),
- "votescast": Decimal("1.000000"),
- "voted_ids": [1],
- },
- )
- self.assert_model_exists(
- "option/1",
- {
- "yes": Decimal("1.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/2",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("1.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/3",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("1.000000"),
- },
- )
-
- def test_vote_with_voteweight(self) -> None:
- self.set_models(
- {
- "meeting_user/11": {"vote_weight": "4.200000"},
- "meeting/113": {"users_enable_vote_weight": True},
- }
- )
- self.add_option()
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": "Y", "2": "N", "3": "A"}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 200)
- self.assert_model_count("vote", 113, 3)
- self.assert_model_exists(
- "poll/1",
- {
- "votesvalid": Decimal("4.200000"),
- "votesinvalid": Decimal("0.000000"),
- "votescast": Decimal("1.000000"),
- "state": Poll.STATE_FINISHED,
- },
- )
- self.assert_model_exists(
- "option/1",
- {
- "yes": Decimal("4.200000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/2",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("4.200000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/3",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("4.200000"),
- },
- )
-
- def test_change_vote(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": "Y"}, "id": 1, "user_id": 1},
- stop_poll_after_vote=False,
- )
- self.assert_status_code(response, 200)
- response = self.request(
- "poll.vote",
- {"value": {"1": "N"}, "id": 1, "user_id": 1},
- start_poll_before_vote=False,
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/2")
- vote = self.get_model("vote/1")
- self.assertEqual(vote.get("value"), "Y")
-
- def test_too_many_options(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": "Y", "3": "N"}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_options(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": "Y", "3": "N"}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_anonymous(self) -> None:
- self.start_poll()
- response = self.anonymous_vote({"value": {"1": "Y"}})
- self.assert_status_code(response, 401)
- self.assert_model_not_exists("vote/1")
-
- def test_vote_not_present(self) -> None:
- self.start_poll()
- self.update_model("meeting/113", {"present_user_ids": None})
- response = self.request(
- "poll.vote",
- {"value": {"1": "Y"}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_state(self) -> None:
- response = self.request(
- "poll.vote",
- {"value": {"1": "Y"}, "id": 1, "user_id": 1},
- start_poll_before_vote=False,
- stop_poll_after_vote=False,
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_missing_data(self) -> None:
- self.start_poll()
- response = self.request("poll.vote", {"value": {}, "id": 1, "user_id": 1})
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
- poll = self.get_model("poll/1")
- self.assertNotIn(1, poll.get("voted_ids", []))
-
- def test_wrong_data_format(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": [1, 2, 5], "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_option_format(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": "string"}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_option_id_type(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"id": "Y"}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_vote_data(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": [None]}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
-
-class VotePollNamedY(VotePollBaseTestClass):
- def create_poll(self) -> None:
- self.create_model(
- "poll/1",
- {
- "content_object_id": "assignment/1",
- "title": "test_title_Zrvh146QAdq7t6iSDwZk",
- "pollmethod": "Y",
- "type": Poll.TYPE_NAMED,
- "state": Poll.STATE_CREATED,
- "meeting_id": 113,
- "votesinvalid": Decimal("0.000000"),
- "global_yes": True,
- "global_no": True,
- "global_abstain": True,
- "min_votes_amount": 1,
- "max_votes_amount": 10,
- "max_votes_per_option": 1,
- "onehundred_percent_base": "Y",
- },
- )
-
- def test_vote(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": 1, "2": 0}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists("vote/1")
- self.assert_model_not_exists("vote/2")
- self.assert_model_exists(
- "poll/1",
- {
- "votesvalid": Decimal("1.000000"),
- "votesinvalid": Decimal("0.000000"),
- "votescast": Decimal("1.000000"),
- "voted_ids": [1],
- },
- )
- self.assert_model_exists(
- "option/1",
- {
- "yes": Decimal("1.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/2",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
-
- def test_change_vote(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": 1, "2": 0}, "id": 1, "user_id": 1},
- stop_poll_after_vote=False,
- )
- self.assert_status_code(response, 200)
- response = self.request(
- "poll.vote",
- {"value": {"1": 0, "2": 1}, "id": 1, "user_id": 1},
- start_poll_before_vote=False,
- )
- self.assert_status_code(response, 400)
- self.assert_model_exists(
- "option/1",
- {
- "yes": Decimal("1.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/2",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
-
- def test_global_yes(self) -> None:
- self.start_poll()
- response = self.request("poll.vote", {"value": "Y", "id": 1, "user_id": 1})
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "option/11",
- {
- "yes": Decimal("1.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
-
- def test_global_yes_forbidden(self) -> None:
- self.update_model("poll/1", {"global_yes": False})
- self.start_poll()
- response = self.request("poll.vote", {"value": "Y", "id": 1, "user_id": 1})
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_global_no(self) -> None:
- self.start_poll()
- response = self.request("poll.vote", {"value": "N", "id": 1, "user_id": 1})
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "option/11",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("1.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
-
- def test_global_no_forbidden(self) -> None:
- self.update_model("poll/1", {"global_no": False})
- self.start_poll()
- response = self.request("poll.vote", {"value": "N", "id": 1, "user_id": 1})
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_global_abstain(self) -> None:
- self.start_poll()
- response = self.request("poll.vote", {"value": "A", "id": 1, "user_id": 1})
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "option/11",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("1.000000"),
- },
- )
-
- def test_global_abstain_forbidden(self) -> None:
- self.update_model("poll/1", {"global_abstain": False})
- self.start_poll()
- response = self.request("poll.vote", {"value": "A", "id": 1, "user_id": 1})
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_negative_vote(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": -1}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_too_many_options(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": 1, "2": 1, "3": 1}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_options(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"id": 1, "value": {"3": 1}},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_anonymous(self) -> None:
- self.start_poll()
- response = self.anonymous_vote({"value": {"1": 1}})
- self.assert_status_code(response, 401)
- self.assert_model_not_exists("vote/1")
-
- def test_anonymous_as_vote_user(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": 1}, "id": 1, "user_id": 0},
- )
- self.assert_status_code(response, 400)
- assert "Votes for anonymous user are not allowed" in response.json["message"]
- self.assert_model_not_exists("vote/1")
-
- def test_vote_not_present(self) -> None:
- self.start_poll()
- self.update_model("meeting/113", {"present_user_ids": None})
- response = self.request(
- "poll.vote",
- {"value": {"1": 1}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_state(self) -> None:
- response = self.request(
- "poll.vote",
- {"value": {"1": 1}, "id": 1, "user_id": 1},
- start_poll_before_vote=False,
- stop_poll_after_vote=False,
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_missing_data(self) -> None:
- self.start_poll()
- response = self.request("poll.vote", {"value": {}, "id": 1, "user_id": 1})
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
- poll = self.get_model("poll/1")
- self.assertNotIn(1, poll.get("voted_ids", []))
-
- def test_wrong_data_format(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": [1, 2, 5], "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_option_format(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": "string"}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_option_id_type(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"id": 1}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_vote_data(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": [None]}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
-
-class VotePollYMaxVotesPerOption(VotePollBaseTestClass):
- def create_poll(self) -> None:
- self.create_model(
- "poll/1",
- {
- "content_object_id": "assignment/1",
- "title": "test_title_Zrvh146QAASDfVeidq7t6iSDwZk",
- "pollmethod": "Y",
- "type": Poll.TYPE_NAMED,
- "state": Poll.STATE_CREATED,
- "meeting_id": 113,
- "votesinvalid": Decimal("0.000000"),
- "global_yes": False,
- "global_no": False,
- "global_abstain": False,
- "min_votes_amount": 1,
- "max_votes_amount": 5,
- "max_votes_per_option": 3,
- "onehundred_percent_base": "Y",
- },
- )
-
- def test_vote(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": 2, "2": 3}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists("vote/1")
- self.assert_model_exists(
- "poll/1",
- {
- "votesvalid": Decimal("1.000000"),
- "votesinvalid": Decimal("0.000000"),
- "votescast": Decimal("1.000000"),
- "voted_ids": [1],
- },
- )
- self.assert_model_exists(
- "option/1",
- {
- "yes": Decimal("2.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/2",
- {
- "yes": Decimal("3.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
-
- def test_change_vote(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": 1, "2": 3}, "id": 1, "user_id": 1},
- stop_poll_after_vote=False,
- )
- response = self.request(
- "poll.vote",
- {"value": {"1": 2, "2": 0}, "id": 1, "user_id": 1},
- start_poll_before_vote=False,
- )
- self.assert_status_code(response, 400)
- self.assert_model_exists(
- "option/1",
- {
- "yes": Decimal("1.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/2",
- {
- "yes": Decimal("3.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
-
- def test_vote_weight(self) -> None:
- self.update_model("user/1", {"default_vote_weight": Decimal("3.000000")})
- self.update_model("meeting/113", {"users_enable_vote_weight": True})
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": 1, "2": 3}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "option/1",
- {
- "yes": Decimal("3.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/2",
- {
- "yes": Decimal("9.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
-
- def test_vote_change_weight(self) -> None:
- self.update_model("user/1", {"default_vote_weight": Decimal("3.000000")})
- self.update_model("meeting/113", {"users_enable_vote_weight": True})
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": 2, "2": 0}, "id": 1, "user_id": 1},
- stop_poll_after_vote=False,
- )
- response = self.request(
- "poll.vote",
- {"value": {"1": 0, "2": 3}, "id": 1, "user_id": 1},
- start_poll_before_vote=False,
- )
- self.assert_status_code(response, 400)
- self.assert_model_exists(
- "option/1",
- {
- "yes": Decimal("6.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/2",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
-
-
-class VotePollNamedN(VotePollBaseTestClass):
- def create_poll(self) -> None:
- self.create_model(
- "poll/1",
- {
- "content_object_id": "assignment/1",
- "title": "test_title_4oi49ckKFk39SDIfj30s",
- "pollmethod": "N",
- "type": Poll.TYPE_NAMED,
- "state": Poll.STATE_CREATED,
- "meeting_id": 113,
- "votesinvalid": Decimal("0.000000"),
- "global_yes": True,
- "global_no": True,
- "global_abstain": True,
- "min_votes_amount": 1,
- "max_votes_amount": 10,
- "max_votes_per_option": 1,
- "onehundred_percent_base": "Y",
- },
- )
-
- def test_vote(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": 1, "2": 0}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists("vote/1")
- self.assert_model_not_exists("vote/2")
- self.assert_model_exists(
- "poll/1",
- {
- "votesvalid": Decimal("1.000000"),
- "votesinvalid": Decimal("0.000000"),
- "votescast": Decimal("1.000000"),
- "voted_ids": [1],
- },
- )
- self.assert_model_exists(
- "option/1",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("1.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/2",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
-
- def test_change_vote(self) -> None:
- self.add_option()
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": 1, "2": 0}, "id": 1, "user_id": 1},
- stop_poll_after_vote=False,
- )
- response = self.request(
- "poll.vote",
- {"value": {"1": 0, "2": 1}, "id": 1, "user_id": 1},
- start_poll_before_vote=False,
- )
- self.assert_status_code(response, 400)
- self.assert_model_exists(
- "option/1",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("1.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/2",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
-
- def test_global_yes(self) -> None:
- self.start_poll()
- response = self.request("poll.vote", {"value": "Y", "id": 1, "user_id": 1})
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "option/11",
- {
- "yes": Decimal("1.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
-
- def test_global_yes_forbidden(self) -> None:
- self.update_model("poll/1", {"global_yes": False})
- self.start_poll()
- response = self.request("poll.vote", {"value": "Y", "id": 1, "user_id": 1})
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_global_no(self) -> None:
- self.start_poll()
- response = self.request("poll.vote", {"value": "N", "id": 1, "user_id": 1})
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "option/11",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("1.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
-
- def test_global_no_forbidden(self) -> None:
- self.update_model("poll/1", {"global_no": False})
- self.start_poll()
- response = self.request("poll.vote", {"value": "N", "id": 1, "user_id": 1})
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_global_abstain(self) -> None:
- self.start_poll()
- response = self.request("poll.vote", {"value": "A", "id": 1, "user_id": 1})
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "option/11",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("1.000000"),
- },
- )
-
- def test_global_abstain_forbidden(self) -> None:
- self.update_model("poll/1", {"global_abstain": False})
- self.start_poll()
- response = self.request("poll.vote", {"value": "A", "id": 1, "user_id": 1})
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_negative_vote(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": -1}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_options(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"3": 1}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_anonymous(self) -> None:
- self.start_poll()
- response = self.anonymous_vote({"value": {"1": 1}})
- self.assert_status_code(response, 401)
- self.assert_model_not_exists("vote/1")
-
- def test_vote_not_present(self) -> None:
- self.start_poll()
- self.update_model("meeting/113", {"present_user_ids": None})
- response = self.request(
- "poll.vote",
- {"value": {"1": 1}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_state(self) -> None:
- response = self.request(
- "poll.vote",
- {"value": {"1": 1}, "id": 1, "user_id": 1},
- start_poll_before_vote=False,
- stop_poll_after_vote=False,
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_missing_data(self) -> None:
- self.start_poll()
- response = self.request("poll.vote", {"value": {}, "id": 1, "user_id": 1})
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
- poll = self.get_model("poll/1")
- self.assertNotIn(1, poll.get("voted_ids", []))
-
- def test_wrong_data_format(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": [1, 2, 5], "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_option_format(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": "string"}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_option_id_type(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"id": 1}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_vote_data(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": [None]}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
-
-class VotePollPseudoanonymousYNA(VotePollBaseTestClass):
- def create_poll(self) -> None:
- self.create_model(
- "poll/1",
- {
- "content_object_id": "assignment/1",
- "title": "test_title_OkHAIvOSIcpFnCxbaL6v",
- "pollmethod": "YNA",
- "type": Poll.TYPE_PSEUDOANONYMOUS,
- "state": Poll.STATE_CREATED,
- "meeting_id": 113,
- "votesinvalid": Decimal("0.000000"),
- "min_votes_amount": 1,
- "max_votes_amount": 10,
- "max_votes_per_option": 1,
- "onehundred_percent_base": "Y",
- },
- )
-
- def test_vote(self) -> None:
- self.add_option()
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": "Y", "2": "N", "3": "A"}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 200)
- self.assert_model_count("vote", 113, 3)
- self.assert_model_exists(
- "poll/1",
- {
- "votesvalid": Decimal("1.000000"),
- "votesinvalid": Decimal("0.000000"),
- "votescast": Decimal("1.000000"),
- },
- )
- self.assert_model_exists(
- "option/1",
- {
- "yes": Decimal("1.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/2",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("1.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/3",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("1.000000"),
- },
- )
-
- def test_change_vote(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": "Y"}, "id": 1, "user_id": 1},
- stop_poll_after_vote=False,
- )
- response = self.request(
- "poll.vote",
- {"value": {"1": "N"}, "id": 1, "user_id": 1},
- start_poll_before_vote=False,
- )
- self.assert_status_code(response, 400)
- self.assert_model_exists(
- "option/1",
- {
- "yes": Decimal("1.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
-
- def test_too_many_options(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": "Y", "3": "N"}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_partial_vote(self) -> None:
- self.add_option()
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": "Y"}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists("vote/1")
-
- def test_wrong_options(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": "Y", "3": "N"}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_anonymous(self) -> None:
- self.start_poll()
- response = self.anonymous_vote({"value": {"1": "Y"}})
- self.assert_status_code(response, 401)
- self.assert_model_not_exists("vote/1")
-
- def test_vote_not_present(self) -> None:
- self.start_poll()
- self.update_model("meeting/113", {"present_user_ids": None})
- response = self.request(
- "poll.vote",
- {"value": {"1": "Y"}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_state(self) -> None:
- response = self.request(
- "poll.vote",
- {"value": {}, "id": 1, "user_id": 1},
- start_poll_before_vote=False,
- stop_poll_after_vote=False,
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_missing_value(self) -> None:
- self.start_poll()
- response = self.request("poll.vote", {"value": {}, "id": 1, "user_id": 1})
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
- poll = self.get_model("poll/1")
- self.assertNotIn(1, poll.get("voted_ids", []))
-
- def test_wrong_value_format(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": [1, 2, 5], "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_option_format(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": "string"}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_option_id_type(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"id": "Y"}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_vote_value(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": [None]}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
-
-class VotePollPseudoanonymousY(VotePollBaseTestClass):
- def create_poll(self) -> None:
- self.create_model(
- "poll/1",
- {
- "content_object_id": "assignment/1",
- "title": "test_title_Zrvh146QAdq7t6iSDwZk",
- "pollmethod": "Y",
- "type": Poll.TYPE_PSEUDOANONYMOUS,
- "state": Poll.STATE_CREATED,
- "meeting_id": 113,
- "votesinvalid": Decimal("0.000000"),
- "min_votes_amount": 1,
- "max_votes_amount": 10,
- "max_votes_per_option": 1,
- "onehundred_percent_base": "Y",
- },
- )
-
- def test_vote(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": 1, "2": 0}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists("vote/1")
- self.assert_model_not_exists("vote/2")
- self.assert_model_exists(
- "poll/1",
- {
- "votesvalid": Decimal("1.000000"),
- "votesinvalid": Decimal("0.000000"),
- "votescast": Decimal("1.000000"),
- "voted_ids": [1],
- },
- )
- self.assert_model_exists(
- "option/1",
- {
- "yes": Decimal("1.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/2",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists("vote/1", {"user_id": None})
-
- def test_change_vote(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": 1, "2": 0}, "id": 1, "user_id": 1},
- stop_poll_after_vote=False,
- )
- response = self.request(
- "poll.vote",
- {"value": {"1": 0, "2": 1}, "id": 1, "user_id": 1},
- start_poll_before_vote=False,
- )
- self.assert_status_code(response, 400)
- self.get_model("poll/1")
- self.assert_model_exists(
- "option/1",
- {
- "yes": Decimal("1.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/2",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
-
- def test_negative_vote(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": -1}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_options(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"3": 1}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_vote_not_present(self) -> None:
- self.start_poll()
- self.update_model("meeting/113", {"present_user_ids": None})
- response = self.request(
- "poll.vote",
- {"value": {"1": 1}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_state(self) -> None:
- response = self.request(
- "poll.vote",
- {"value": {"1": 1}, "id": 1, "user_id": 1},
- start_poll_before_vote=False,
- stop_poll_after_vote=False,
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_missing_data(self) -> None:
- self.start_poll()
- response = self.request("poll.vote", {"value": {}, "id": 1, "user_id": 1})
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
- poll = self.get_model("poll/1")
- self.assertNotIn(1, poll.get("voted_ids", []))
-
- def test_wrong_data_format(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"value": [1, 2, 5]}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_option_format(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": "string"}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_option_id_type(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"id": 1}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_vote_data(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": [None]}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
-
-class VotePollPseudoanonymousN(VotePollBaseTestClass):
- def create_poll(self) -> None:
- self.create_model(
- "poll/1",
- {
- "content_object_id": "assignment/1",
- "title": "test_title_wWPOVJgL9afm83eamf3e",
- "pollmethod": "N",
- "type": Poll.TYPE_PSEUDOANONYMOUS,
- "state": Poll.STATE_CREATED,
- "meeting_id": 113,
- "votesinvalid": Decimal("0.000000"),
- "min_votes_amount": 1,
- "max_votes_amount": 10,
- "max_votes_per_option": 1,
- "onehundred_percent_base": "Y",
- },
- )
-
- def test_vote(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"id": 1, "value": {"1": 1, "2": 0}, "user_id": 1},
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists("vote/1")
- self.assert_model_not_exists("vote/2")
- self.assert_model_exists(
- "poll/1",
- {
- "votesvalid": Decimal("1.000000"),
- "votesinvalid": Decimal("0.000000"),
- "votescast": Decimal("1.000000"),
- "voted_ids": [1],
- },
- )
- self.assert_model_exists(
- "option/1",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("1.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/2",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists("vote/1", {"user_id": None})
-
- def test_change_vote(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": 1, "2": 0}, "id": 1, "user_id": 1},
- stop_poll_after_vote=False,
- )
- response = self.request(
- "poll.vote",
- {"value": {"1": 0, "2": 1}, "id": 1, "user_id": 1},
- start_poll_before_vote=False,
- )
- self.assert_status_code(response, 400)
- self.get_model("poll/1")
- self.assert_model_exists(
- "option/1",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("1.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
- self.assert_model_exists(
- "option/2",
- {
- "yes": Decimal("0.000000"),
- "no": Decimal("0.000000"),
- "abstain": Decimal("0.000000"),
- },
- )
-
- def test_negative_vote(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": -1}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_vote_not_present(self) -> None:
- self.start_poll()
- self.update_model("meeting/113", {"present_user_ids": None})
-
- response = self.request(
- "poll.vote",
- {"id": 1, "user_id": 1, "value": {"1": 1}},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_state(self) -> None:
- response = self.request(
- "poll.vote",
- {"value": {"1": 1}, "id": 1, "user_id": 1},
- start_poll_before_vote=False,
- stop_poll_after_vote=False,
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_data_format(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": [1, 2, 5], "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- assert (
- "decoding payload: unknown vote value: `[1,2,5]`"
- in response.json["message"]
- )
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_option_format(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"value": {"1": "string"}, "id": 1, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- assert "Your vote has a wrong format" in response.json["message"]
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_option_id_type(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"id": 1},
- )
- self.assert_status_code(response, 400)
- self.assert_model_not_exists("vote/1")
-
- def test_wrong_vote_data(self) -> None:
- self.start_poll()
- response = self.request(
- "poll.vote",
- {"id": 1, "value": {"1": [None]}, "user_id": 1},
- )
- self.assert_status_code(response, 400)
- assert "decoding payload: unknown vote value:" in response.json["message"]
- self.assert_model_not_exists("vote/1")
diff --git a/tests/system/action/poll_candidate/__init__.py b/tests/system/action/poll_candidate/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/tests/system/action/poll_candidate/test_create.py b/tests/system/action/poll_candidate/test_create.py
deleted file mode 100644
index 24abbb79b5..0000000000
--- a/tests/system/action/poll_candidate/test_create.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from tests.system.action.base import BaseActionTestCase
-
-
-class PollCandidateTest(BaseActionTestCase):
- def test_create(self) -> None:
- self.create_meeting()
- self.set_models(
- {
- "poll_candidate_list/2": {"meeting_id": 1},
- "option/1": {
- "meeting_id": 1,
- "content_object_id": "poll_candidate_list/2",
- },
- }
- )
- response = self.request(
- "poll_candidate.create",
- {"user_id": 1, "poll_candidate_list_id": 2, "weight": 12, "meeting_id": 1},
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll_candidate/1",
- {"meeting_id": 1, "user_id": 1, "poll_candidate_list_id": 2, "weight": 12},
- )
- self.assert_model_exists("user/1", {"poll_candidate_ids": [1]})
- self.assert_model_exists("meeting/1", {"poll_candidate_ids": [1]})
- self.assert_model_exists("poll_candidate_list/2", {"poll_candidate_ids": [1]})
diff --git a/tests/system/action/poll_candidate/test_delete.py b/tests/system/action/poll_candidate/test_delete.py
deleted file mode 100644
index df2d336c87..0000000000
--- a/tests/system/action/poll_candidate/test_delete.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from tests.system.action.base import BaseActionTestCase
-
-
-class PollCandidateDeleteTest(BaseActionTestCase):
- def test_delete_correct(self) -> None:
- self.create_meeting()
- self.set_models(
- {
- "poll_candidate_list/2": {"meeting_id": 1},
- "option/1": {
- "meeting_id": 1,
- "content_object_id": "poll_candidate_list/2",
- },
- "poll_candidate/3": {
- "meeting_id": 1,
- "poll_candidate_list_id": 2,
- "user_id": 1,
- "weight": 1,
- },
- }
- )
- response = self.request("poll_candidate.delete", {"id": 3})
- self.assert_status_code(response, 200)
- self.assert_model_not_exists("poll_candidate/3")
- self.assert_model_exists("user/1", {"poll_candidate_ids": None})
- self.assert_model_exists("poll_candidate_list/2", {"poll_candidate_ids": None})
- self.assert_model_exists("meeting/1", {"poll_candidate_ids": None})
diff --git a/tests/system/action/poll_candidate_list/__init__.py b/tests/system/action/poll_candidate_list/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/tests/system/action/poll_candidate_list/test_create.py b/tests/system/action/poll_candidate_list/test_create.py
deleted file mode 100644
index 6a42ea0da0..0000000000
--- a/tests/system/action/poll_candidate_list/test_create.py
+++ /dev/null
@@ -1,38 +0,0 @@
-from tests.system.action.base import BaseActionTestCase
-
-
-class PollCandidateList(BaseActionTestCase):
- def test_create_correct(self) -> None:
- self.create_meeting()
- self.create_user("test1")
- self.create_user("test2")
- self.set_models({"option/4": {"meeting_id": 1}})
- response = self.request(
- "poll_candidate_list.create",
- {
- "option_id": 4,
- "meeting_id": 1,
- "entries": [{"user_id": 2, "weight": 1}, {"user_id": 3, "weight": 2}],
- },
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "poll_candidate_list/1",
- {"option_id": 4, "meeting_id": 1, "poll_candidate_ids": [1, 2]},
- )
- self.assert_model_exists(
- "poll_candidate/1",
- {"user_id": 2, "meeting_id": 1, "poll_candidate_list_id": 1, "weight": 1},
- )
- self.assert_model_exists(
- "poll_candidate/2",
- {"user_id": 3, "meeting_id": 1, "poll_candidate_list_id": 1, "weight": 2},
- )
- self.assert_model_exists("user/2", {"poll_candidate_ids": [1]})
- self.assert_model_exists("user/3", {"poll_candidate_ids": [2]})
- self.assert_model_exists(
- "meeting/1", {"poll_candidate_list_ids": [1], "poll_candidate_ids": [1, 2]}
- )
- self.assert_model_exists(
- "option/4", {"content_object_id": "poll_candidate_list/1"}
- )
diff --git a/tests/system/action/poll_candidate_list/test_delete.py b/tests/system/action/poll_candidate_list/test_delete.py
deleted file mode 100644
index a8f269f964..0000000000
--- a/tests/system/action/poll_candidate_list/test_delete.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from tests.system.action.base import BaseActionTestCase
-
-
-class PollCandidateListDeleteTest(BaseActionTestCase):
- def test_delete_correct(self) -> None:
- self.create_meeting()
- self.create_user("test1")
- self.create_user("test2")
- self.set_models(
- {
- "poll_candidate_list/2": {"meeting_id": 1},
- "option/1": {
- "meeting_id": 1,
- "content_object_id": "poll_candidate_list/2",
- },
- "poll_candidate/3": {
- "meeting_id": 1,
- "poll_candidate_list_id": 2,
- "user_id": 1,
- "weight": 1,
- },
- "poll_candidate/4": {
- "meeting_id": 1,
- "poll_candidate_list_id": 2,
- "user_id": 2,
- "weight": 2,
- },
- "poll_candidate/5": {
- "meeting_id": 1,
- "poll_candidate_list_id": 2,
- "user_id": 3,
- "weight": 3,
- },
- }
- )
- response = self.request("poll_candidate_list.delete", {"id": 2})
- self.assert_status_code(response, 200)
- self.assert_model_not_exists("poll_candidate_list/2")
- for poll_candidate_id in range(3, 6):
- self.assert_model_not_exists(f"poll_candidate/{poll_candidate_id}")
- for user_id in range(1, 4):
- self.assert_model_exists(f"user/{user_id}", {"poll_candidate_ids": None})
- self.assert_model_exists(
- "meeting/1", {"poll_candidate_list_ids": None, "poll_candidate_ids": None}
- )
diff --git a/tests/system/action/projector/test_toggle.py b/tests/system/action/projector/test_toggle.py
index 4893c26219..55fc86f6ef 100644
--- a/tests/system/action/projector/test_toggle.py
+++ b/tests/system/action/projector/test_toggle.py
@@ -16,12 +16,15 @@ def create_poll(self, base: int) -> None:
f"poll/{base}": {
"meeting_id": 1,
"title": "A very important change",
- "type": Poll.TYPE_PSEUDOANONYMOUS,
- "backend": "fast",
- "pollmethod": "YN",
- "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_YN,
+ "visibility": Poll.VISIBILITY_SECRET,
+ "config_id": f"poll_config_rating_approval/{base}",
+ "state": Poll.STATE_STARTED,
"content_object_id": "motion/1",
- }
+ },
+ f"poll_config_rating_approval/{base}": {
+ "poll_id": base,
+ "allow_abstain": False,
+ },
}
)
diff --git a/tests/system/action/test_internal_actions.py b/tests/system/action/test_internal_actions.py
index bac520f8a1..2fcd22c20c 100644
--- a/tests/system/action/test_internal_actions.py
+++ b/tests/system/action/test_internal_actions.py
@@ -210,12 +210,12 @@ def test_internal_execute_stack_internal_action(self) -> None:
def test_internal_execute_backend_internal_action(self) -> None:
response = self.internal_request(
- "option.create",
- {"meeting_id": 1, "text": "test"},
+ "meeting_user.create",
+ {"meeting_id": 1, "user_id": 1},
self.internal_auth_password,
)
self.assert_status_code(response, 400)
self.assertEqual(
- response.json.get("message"), "Action option.create does not exist."
+ response.json.get("message"), "Action meeting_user.create does not exist."
)
- self.assert_model_not_exists("option/1")
+ self.assert_model_not_exists("meeting_user/1")
diff --git a/tests/system/action/user/test_delete.py b/tests/system/action/user/test_delete.py
index a1607959be..0facbb9d44 100644
--- a/tests/system/action/user/test_delete.py
+++ b/tests/system/action/user/test_delete.py
@@ -1,6 +1,7 @@
from datetime import datetime
from zoneinfo import ZoneInfo
+from openslides_backend.models.models import Poll
from openslides_backend.permissions.management_levels import OrganizationManagementLevel
from openslides_backend.permissions.permissions import Permissions
from openslides_backend.shared.util import ONE_ORGANIZATION_FQID
@@ -192,30 +193,38 @@ def test_delete_with_submitter(self) -> None:
def test_delete_with_poll_candidate(self) -> None:
self.create_meeting()
+ user_id = self.create_user_for_meeting(1)
self.set_models(
{
- "user/111": {
- "username": "username_srtgb123",
- "poll_candidate_ids": [34],
+ "poll/1": {
+ "title": "Poll 1",
+ "meeting_id": 1,
+ "content_object_id": "assignment/1",
+ "visibility": Poll.VISIBILITY_NAMED,
+ "config_id": "poll_config_approval/1",
+ "state": Poll.STATE_FINISHED,
},
- "poll_candidate/34": {
- "user_id": 111,
- "poll_candidate_list_id": 1,
- "weight": 1,
+ "list_of_speakers/1": {
"meeting_id": 1,
+ "content_object_id": "assignment/1",
},
- "poll_candidate_list/1": {"meeting_id": 1},
- "option/1": {
+ "assignment/1": {
+ "id": 1,
+ "title": "Duckburg town council",
"meeting_id": 1,
- "content_object_id": "poll_candidate_list/1",
},
+ "poll_config_option/1": {
+ "poll_config_id": "poll_config_approval/1",
+ "meeting_user_id": 1,
+ },
+ "poll_config_approval/1": {"poll_id": 1},
}
)
- response = self.request("user.delete", {"id": 111})
-
+ response = self.request("user.delete", {"id": user_id})
self.assert_status_code(response, 200)
- self.assert_model_not_exists("user/111")
- self.assert_model_exists("poll_candidate/34", {"user_id": None})
+ self.assert_model_not_exists(f"user/{user_id}")
+ self.assert_model_not_exists("meeting_user/1")
+ self.assert_model_exists("poll_config_option/1", {"meeting_user_id": None})
def test_delete_with_group_ids_set_null(self) -> None:
self.create_meeting()
diff --git a/tests/system/action/user/test_merge_together.py b/tests/system/action/user/test_merge_together.py
index a6a8a5ff52..9f305e4afb 100644
--- a/tests/system/action/user/test_merge_together.py
+++ b/tests/system/action/user/test_merge_together.py
@@ -5,9 +5,12 @@
from typing import Any, Literal, cast
from zoneinfo import ZoneInfo
+from psycopg.types.json import Jsonb
+
from openslides_backend.action.actions.speaker.speech_state import SpeechState
from openslides_backend.action.relations.relation_manager import RelationManager
from openslides_backend.action.util.actions_map import actions_map
+from openslides_backend.models.models import Poll
from openslides_backend.permissions.management_levels import OrganizationManagementLevel
from openslides_backend.shared.patterns import (
CollectionField,
@@ -835,6 +838,9 @@ def set_up_polls_for_merge(self) -> None:
self.create_assignment(1, 1)
self.create_motion(4, 4)
self.create_topic(7, 7)
+ self.create_assignment(1, 1)
+ self.create_motion(4, 1)
+ self.create_topic(7, 7)
self.set_models(
{
"meeting/1": {"present_user_ids": [2, 4]},
@@ -843,6 +849,7 @@ def set_up_polls_for_merge(self) -> None:
"meeting/10": {"present_user_ids": [5]},
"meeting_user/15": {"vote_delegated_to_id": 14},
"motion_state/4": {"allow_create_poll": True},
+ "motion_state/4": {"allow_create_poll": True},
"motion_submitter/1": {
"id": 1,
"weight": 1,
@@ -852,124 +859,211 @@ def set_up_polls_for_merge(self) -> None:
},
}
)
- self.request_multi(
- "poll.create",
- [
- {
- "title": "Assignment poll 1",
- "content_object_id": "assignment/1",
- "type": "named",
- "pollmethod": "Y",
- "meeting_id": 1,
- "options": [
- {"content_object_id": "user/2"},
- {"content_object_id": "user/5"},
- ],
- "global_no": True,
- "min_votes_amount": 1,
- "max_votes_amount": 2,
- "max_votes_per_option": 1,
- "backend": "long",
- "entitled_group_ids": [1, 2, 3],
- },
- {
- "title": "Assignment poll 2",
- "content_object_id": "assignment/1",
- "type": "named",
- "pollmethod": "YN",
- "meeting_id": 1,
- "options": [
- {"poll_candidate_user_ids": [5, 4]},
- ],
- "min_votes_amount": 1,
- "max_votes_amount": 1,
- "max_votes_per_option": 1,
- "backend": "fast",
- "entitled_group_ids": [1, 2, 3],
- },
- {
- "title": "Assignment poll 3",
- "content_object_id": "assignment/1",
- "type": "named",
- "pollmethod": "YN",
- "meeting_id": 1,
- "options": [
- {"poll_candidate_user_ids": [2, 5]},
- ],
- "min_votes_amount": 1,
- "max_votes_amount": 1,
- "max_votes_per_option": 1,
- "backend": "fast",
- "entitled_group_ids": [1, 2, 3],
- },
- {
- "title": "Assignment poll 4",
- "content_object_id": "assignment/1",
- "type": "pseudoanonymous",
- "pollmethod": "Y",
- "meeting_id": 1,
- "options": [
- {"content_object_id": "user/4"},
- {"content_object_id": "user/5"},
- ],
- "min_votes_amount": 1,
- "max_votes_amount": 2,
- "max_votes_per_option": 1,
- "backend": "long",
- "entitled_group_ids": [1, 2, 3],
- },
- {
- "title": "Motion poll",
- "content_object_id": "motion/4",
- "type": "named",
- "pollmethod": "YNA",
- "meeting_id": 4,
- "options": [
- {"content_object_id": "motion/4"},
- ],
- "min_votes_amount": 1,
- "max_votes_amount": 1,
- "max_votes_per_option": 1,
- "backend": "fast",
- "entitled_group_ids": [4, 5, 6],
- },
- {
- "title": "Topic poll",
- "content_object_id": "topic/7",
- "type": "pseudoanonymous",
- "pollmethod": "Y",
- "meeting_id": 7,
- "options": [
- {"text": "Option 1"},
- {"text": "Option 2"},
- {"text": "Option 3"},
- ],
- "min_votes_amount": 1,
- "max_votes_amount": 3,
- "max_votes_per_option": 1,
- "backend": "fast",
- "entitled_group_ids": [7, 8, 9],
- },
- ],
+ polls_data = {
+ "poll/1": {
+ "title": "Assignment poll 1",
+ "content_object_id": "assignment/1",
+ "visibility": Poll.VISIBILITY_NAMED,
+ "config_id": "poll_config_selection/1",
+ "state": Poll.STATE_STARTED,
+ "meeting_id": 1,
+ "entitled_group_ids": [1, 2, 3],
+ },
+ "poll/2": {
+ "title": "Assignment poll 2",
+ "content_object_id": "assignment/1",
+ "visibility": Poll.VISIBILITY_NAMED,
+ "config_id": "poll_config_approval/2",
+ "state": Poll.STATE_STARTED,
+ "meeting_id": 1,
+ "entitled_group_ids": [1, 2, 3],
+ },
+ "poll/3": {
+ "title": "Assignment poll 3",
+ "content_object_id": "assignment/1",
+ "visibility": Poll.VISIBILITY_NAMED,
+ "config_id": "poll_config_approval/3",
+ "state": Poll.STATE_STARTED,
+ "meeting_id": 1,
+ "entitled_group_ids": [1, 2, 3],
+ },
+ "poll/4": {
+ "title": "Assignment poll 4",
+ "content_object_id": "assignment/1",
+ "visibility": Poll.VISIBILITY_SECRET,
+ "config_id": "poll_config_selection/4",
+ "state": Poll.STATE_STARTED,
+ "meeting_id": 1,
+ "entitled_group_ids": [1, 2, 3],
+ },
+ "poll/5": {
+ "title": "Motion poll",
+ "content_object_id": "motion/1",
+ "visibility": Poll.VISIBILITY_NAMED,
+ "config_id": "poll_config_approval/5",
+ "state": Poll.STATE_STARTED,
+ "meeting_id": 4,
+ "entitled_group_ids": [4, 5, 6],
+ },
+ "poll/6": {
+ "title": "Topic poll",
+ "content_object_id": "topic/7",
+ "visibility": Poll.VISIBILITY_SECRET,
+ "config_id": "poll_config_rating_score/6",
+ "state": Poll.STATE_STARTED,
+ "meeting_id": 7,
+ "entitled_group_ids": [7, 8, 9],
+ },
+ }
+ # add entitled groups
+ configs_data = {
+ **{
+ f"group/{group_id}": {"poll_ids": list(range(1, 5))}
+ for group_id in range(1, 4)
+ },
+ "poll_config_selection/1": {
+ "poll_id": 1,
+ "min_options_amount": 1,
+ "max_options_amount": 2,
+ },
+ "poll_config_option/112": {
+ "poll_config_id": "poll_config_selection/1",
+ "meeting_user_id": 12,
+ },
+ "poll_config_option/115": {
+ "poll_config_id": "poll_config_selection/1",
+ "meeting_user_id": 15,
+ },
+ "poll_config_approval/2": {"poll_id": 2, "allow_abstain": False},
+ "poll_config_option/214": {
+ "poll_config_id": "poll_config_approval/2",
+ "meeting_user_id": 14,
+ },
+ "poll_config_option/215": {
+ "poll_config_id": "poll_config_approval/2",
+ "meeting_user_id": 15,
+ },
+ "poll_config_approval/3": {"poll_id": 3, "allow_abstain": False},
+ "poll_config_option/312": {
+ "poll_config_id": "poll_config_approval/3",
+ "meeting_user_id": 12,
+ },
+ "poll_config_option/315": {
+ "poll_config_id": "poll_config_approval/3",
+ "meeting_user_id": 15,
+ },
+ "poll_config_selection/4": {
+ "poll_id": 4,
+ "min_options_amount": 1,
+ "max_options_amount": 2,
+ },
+ "poll_config_option/414": {
+ "poll_config_id": "poll_config_selection/4",
+ "meeting_user_id": 14,
+ },
+ "poll_config_option/415": {
+ "poll_config_id": "poll_config_selection/4",
+ "meeting_user_id": 15,
+ },
+ "poll_config_approval/5": {"poll_id": 5},
+ "poll_config_rating_score/6": {
+ "poll_id": 6,
+ "min_options_amount": 1,
+ "max_options_amount": 3,
+ "max_votes_per_option": 1,
+ },
+ **{
+ f"poll_config_option/{poll_6_option_id}": {
+ "poll_config_id": "poll_config_rating_score/6",
+ "text": f"Option {poll_6_option_id}",
+ }
+ for poll_6_option_id in range(61, 64)
+ },
+ "meeting_user/13": {
+ "user_id": 3,
+ "meeting_id": 1,
+ "group_ids": [1],
+ "vote_weight": Decimal("1"),
+ },
+ }
+ self.set_models(polls_data)
+ self.set_models(configs_data)
+
+ # def vote(self, poll_id: int, payload: dict[str, Any]) -> None:
+ # self.vote_service.vote(poll_id, payload)
+ def vote(
+ self,
+ base: int,
+ poll_id: int,
+ value: Any, # TODO: narrow down
+ acting_meeting_user_id: int | None = None,
+ represented_meeting_user_id: int | None = None,
+ is_secret: bool = False,
+ ) -> None:
+ mu_data = (
+ {
+ "acting_meeting_user_id": acting_meeting_user_id,
+ "represented_meeting_user_id": represented_meeting_user_id
+ or acting_meeting_user_id,
+ }
+ if not is_secret
+ else {}
+ )
+ self.set_models(
+ {
+ f"ballot/{base}": {
+ "poll_id": poll_id,
+ "value": value,
+ **mu_data,
+ }
+ }
)
- def create_polls_with_correct_votes(self) -> None:
+ def create_polls_with_correct_votes(self, poll_4_is_running: bool = False) -> None:
self.set_up_polls_for_merge()
- self.request_multi("poll.start", [{"id": i} for i in [3, 4]])
- self.login(4)
- self.request("poll.vote", {"id": 1, "value": "N"}, stop_poll_after_vote=False) # type: ignore
- self.request(
- "poll.vote",
- {"id": 1, "value": "N", "user_id": 5},
- start_poll_before_vote=False,
- )
- self.login(2)
- self.request("poll.vote", {"id": 2, "value": {"4": "Y"}})
- self.login(3)
- self.request("poll.vote", {"id": 5, "value": {"11": "A"}})
- self.request("poll.vote", {"id": 6, "value": {"13": 1, "14": 1, "15": 0}})
+ self.vote(1, 1, [112, 115], 14)
+ self.vote(2, 1, [112], 14, 15)
+ self.vote(3, 2, "yes", 12)
+ # secret
+ self.vote(4, 5, "abstain", 43, is_secret=True)
+ self.vote(5, 6, Jsonb({"1": 1, "2": 1, "3": 0}), 73, is_secret=True)
+
self.login(1)
- self.request_multi("poll.stop", [{"id": i} for i in [3, 4]])
+ self.set_models(
+ {
+ "meeting_user/12": {"poll_voted_ids": [2]},
+ "meeting_user/43": {"poll_voted_ids": [5]},
+ "meeting_user/73": {"poll_voted_ids": [6]},
+ "meeting_user/14": {"poll_voted_ids": [1]},
+ "meeting_user/15": {"poll_voted_ids": [1]},
+ }
+ )
+ self.set_models(
+ {
+ f"poll/{poll_id}": {"state": Poll.STATE_FINISHED, "published": True}
+ for poll_id in range(1, 7)
+ if not (poll_id == 4 and poll_4_is_running)
+ }
+ )
+ # self.login(4)
+ # self.vote(1, {"value": [112, 115]})
+ # self.vote(1, {"value": [112], "meeting_user_id": 15})
+
+ # self.login(2)
+ # self.vote(2, {"value": "yes"})
+
+ # self.login(3)
+ # self.vote(5, {"value": "abstain"})
+ # self.vote(6, {"value": {"1": 1, "2": 1, "3": 0}})
+
+ # self.login(1)
+ # self.set_models(
+ # {
+ # f"poll/{poll_id}": {"state": Poll.STATE_FINISHED, "published": True}
+ # for poll_id in range(1, 7)
+ # if not (poll_id == 4 and poll_4_is_running)
+ # }
+ # )
def assert_merge_with_polls_correct(
self, password: str, add_to_creatable_ids: int = 0
@@ -985,15 +1079,21 @@ def assert_merge_with_polls_correct(
"default_password": "user2",
"meeting_user_ids": [12, 42, 106 + add_to_creatable_ids],
"password": password,
- "poll_candidate_ids": [2, 3],
- "option_ids": [1, 8],
- "poll_voted_ids": [1, 2, 5, 6],
- "vote_ids": [1, 3, 4],
- "delegated_vote_ids": [1, 2, 3, 4],
},
)
self.assert_model_exists("committee/60", {"user_ids": [2, 5]})
self.assert_model_exists("committee/66", {"user_ids": [2]})
+ self.assert_model_exists(
+ "meeting_user/12",
+ {
+ "poll_option_ids": [112, 214, 312, 414],
+ "poll_voted_ids": [1, 2],
+ "acting_ballot_ids": [1, 2, 3],
+ "represented_ballot_ids": [1, 3],
+ "vote_delegations_from_ids": [15],
+ },
+ )
+
for id_ in range(3, 5):
self.assert_model_not_exists(f"user/{id_}")
for id_ in [43, 73, 14, 44, 74, *range(106, 106 + add_to_creatable_ids)]:
@@ -1004,27 +1104,52 @@ def assert_merge_with_polls_correct(
)
self.assert_model_exists(
"meeting_user/42",
- {"user_id": 2, "meeting_id": 4, "motion_submitter_ids": [1]},
+ {
+ "user_id": 2,
+ "meeting_id": 4,
+ "motion_submitter_ids": [2],
+ "poll_option_ids": None,
+ "poll_voted_ids": [5],
+ "acting_ballot_ids": None,
+ "represented_ballot_ids": None,
+ },
+ )
+ self.assert_model_exists(
+ "meeting_user/106",
+ {
+ "user_id": 2,
+ "meeting_id": 7,
+ "motion_submitter_ids": None,
+ "poll_option_ids": None,
+ "poll_voted_ids": [6],
+ "acting_ballot_ids": None,
+ "represented_ballot_ids": None,
+ },
)
self.assert_model_not_exists("motion_submitter/2")
self.assert_model_exists(
"motion_submitter/1",
{"motion_id": 4, "meeting_user_id": 42, "meeting_id": 4, "weight": 1},
)
- self.assert_model_exists("poll_candidate/2", {"user_id": 2})
- self.assert_model_exists("poll_candidate/3", {"user_id": 2})
- self.assert_model_exists("vote/2", {"user_id": 5, "delegated_user_id": 2})
- for id_ in [1, 3, 4]:
+ for id_ in [1, 3]:
self.assert_model_exists(
- f"vote/{id_}", {"user_id": 2, "delegated_user_id": 2}
+ f"ballot/{id_}",
+ {"acting_meeting_user_id": 12, "represented_meeting_user_id": 12},
)
- for id_ in [5, 6]: # pseudoanonymous options
+ self.assert_model_exists(
+ "ballot/2",
+ {"acting_meeting_user_id": 12, "represented_meeting_user_id": 15},
+ )
+ for id_ in [4, 5]: # votes from secret voting
+ self.assert_model_exists(
+ f"ballot/{id_}",
+ {"acting_meeting_user_id": None, "represented_meeting_user_id": None},
+ )
+
+ for id_ in [112, 214, 312, 414]:
self.assert_model_exists(
- f"vote/{id_}",
- {"option_id": id_ + 8, "user_id": None, "delegated_user_id": None},
+ f"poll_config_option/{id_}", {"meeting_user_id": 12}
)
- self.assert_model_exists("option/1", {"content_object_id": "user/2"})
- self.assert_model_exists("option/8", {"content_object_id": "user/2"})
def build_expected_user_dates(
voted_present_user_delegated_merged: list[
@@ -1043,69 +1168,15 @@ def build_expected_user_dates(
for date in voted_present_user_delegated_merged
]
- self.assert_model_exists(
- "poll/1",
- {
- "voted_ids": [2, 5],
- "entitled_users_at_stop": build_expected_user_dates(
- [
- (False, True, 2, None, None, None),
- (True, True, 4, None, 2, None),
- (True, False, 5, 4, None, 2),
- ]
- ),
- },
- )
- self.assert_model_exists(
- "poll/2",
- {
- "voted_ids": [2],
- "entitled_users_at_stop": build_expected_user_dates(
- [
- (True, True, 2, None, None, None),
- (False, True, 4, None, 2, None),
- (False, False, 5, 4, None, 2),
- ]
- ),
- },
- )
- for id_ in [3, 4]:
+ for poll_id, meeting_user_id in {2: 12, 5: 42, 6: 106}.items():
self.assert_model_exists(
- f"poll/{id_}",
- {
- "voted_ids": None,
- "entitled_users_at_stop": build_expected_user_dates(
- [
- (False, True, 2, None, None, None),
- (False, True, 4, None, 2, None),
- (False, False, 5, 4, None, 2),
- ]
- ),
- },
+ f"poll/{poll_id}", {"voted_ids": [meeting_user_id]}
)
- self.assert_model_exists(
- "poll/5",
- {
- "voted_ids": [2],
- "entitled_users_at_stop": build_expected_user_dates(
- [
- (False, False, 2, None, None, None),
- (True, True, 3, None, 2, None),
- (False, True, 4, None, 2, None),
- ]
- ),
- },
- )
- self.assert_model_exists(
- "poll/6",
- {
- "voted_ids": [2],
- "entitled_users_at_stop": build_expected_user_dates(
- [(True, True, 3, None, 2, None), (False, True, 4, None, 2, None)]
- ),
- },
- )
+ self.assert_model_exists("poll/1", {"voted_ids": [12, 15]})
+ for id_ in [3, 4]:
+ self.assert_model_exists(f"poll/{id_}", {"voted_ids": None})
+ # @pytest.mark.skip(reason="Finalize and unskip when new vote service is ready")
def test_merge_with_polls_correct(self) -> None:
password = self.assert_model_exists("user/2")["password"]
self.create_polls_with_correct_votes()
@@ -1113,32 +1184,22 @@ def test_merge_with_polls_correct(self) -> None:
self.assert_status_code(response, 200)
self.assert_merge_with_polls_correct(password)
+ # @pytest.mark.skip(reason="Finalize and unskip when new vote service is ready")
def test_merge_with_polls_and_subsequent_merges(self) -> None:
password = self.assert_model_exists("user/2")["password"]
self.create_polls_with_correct_votes()
+ e = self.datastore.get_everything()
response = self.request("user.merge_together", {"id": 3, "user_ids": [4]})
+ e = self.datastore.get_everything()
self.assert_status_code(response, 200)
response = self.request("user.merge_together", {"id": 2, "user_ids": [3]})
+ e = self.datastore.get_everything()
self.assert_status_code(response, 200)
self.assert_merge_with_polls_correct(password, 1)
+ # @pytest.mark.skip(reason="Finalize and unskip when new vote service is ready")
def test_merge_with_polls_all_errors(self) -> None:
- self.set_up_polls_for_merge()
- self.request_multi("poll.start", [{"id": i} for i in [3, 4]])
- self.login(4)
- self.request("poll.vote", {"id": 1, "value": "N"}, stop_poll_after_vote=False)
- self.request(
- "poll.vote",
- {"id": 1, "value": "N", "user_id": 5},
- start_poll_before_vote=False,
- )
- self.login(2)
- self.request("poll.vote", {"id": 2, "value": {"4": "Y"}})
- self.login(3)
- self.request("poll.vote", {"id": 5, "value": {"11": "A"}})
- self.request("poll.vote", {"id": 6, "value": {"13": 1, "14": 1, "15": 0}})
- self.login(1)
- self.request("poll.stop", {"id": 3})
+ self.create_polls_with_correct_votes(True)
response = self.request("user.merge_together", {"id": 2, "user_ids": [3, 4, 5]})
self.assert_status_code(response, 400)
assert (
@@ -2355,6 +2416,7 @@ def test_merge_archived_normal(self) -> None:
self.archive_all_meetings()
self.test_merge_normal()
+ # @pytest.mark.skip(reason="Finalize and unskip when new vote service is ready")
def test_merge_archived_polls(self) -> None:
password = self.assert_model_exists("user/2")["password"]
self.create_polls_with_correct_votes()
diff --git a/tests/system/action/user/test_update.py b/tests/system/action/user/test_update.py
index 0a35d6626b..31a95dc0c1 100644
--- a/tests/system/action/user/test_update.py
+++ b/tests/system/action/user/test_update.py
@@ -3124,136 +3124,35 @@ def test_partial_group_removal_with_speaker(self) -> None:
def test_update_with_internal_fields(self) -> None:
self.create_meeting()
- self.create_user("dummy2", [1])
- self.create_user("dummy3", [1])
- self.create_topic(1, 1)
- self.set_models(
- {
- "user/1": {
- "username": "boady",
- "poll_candidate_ids": [1],
- "option_ids": [1],
- "vote_ids": [1, 2],
- },
- "user/2": {"username": "john", "delegated_vote_ids": [2]},
- "meeting/1": {
- "poll_ids": [1],
- "option_ids": [1, 2],
- "poll_candidate_list_ids": [1],
- "poll_candidate_ids": [1],
- "vote_ids": [1, 2],
- },
- "poll/1": {
- "title": "pull",
- "type": "analog",
- "pollmethod": "YNA",
- "meeting_id": 1,
- "option_ids": [1, 2],
- "content_object_id": "topic/1",
- },
- "option/1": {
- "meeting_id": 1,
- "vote_ids": [1],
- "content_object_id": "user/1",
- },
- "option/2": {
- "meeting_id": 1,
- "vote_ids": [2],
- "content_object_id": "poll_candidate_list/1",
- },
- "poll_candidate_list/1": {
- "meeting_id": 1,
- "option_id": 2,
- "poll_candidate_ids": [1],
- },
- "poll_candidate/1": {
- "poll_candidate_list_id": 1,
- "meeting_id": 1,
- "user_id": 1,
- "weight": 3,
- },
- "vote/1": {
- "meeting_id": 1,
- "option_id": 1,
- "user_id": 1,
- "user_token": "dfjdskjfksdjf",
- },
- "vote/2": {
- "meeting_id": 1,
- "option_id": 2,
- "user_id": 1,
- "delegated_user_id": 2,
- "user_token": "dfjdskjfksdjf",
- },
- }
- )
+ self.create_user_for_meeting(1)
response = self.request(
"user.update",
{
- "id": 3,
+ "id": 2,
"is_present_in_meeting_ids": [1],
- "option_ids": [1],
- "poll_candidate_ids": [1],
- "poll_voted_ids": [1],
- "vote_ids": [1],
- "delegated_vote_ids": [2],
},
internal=True,
)
self.assert_status_code(response, 200)
- expected: dict[str, dict[str, Any]] = {
- "user/3": {
- "is_present_in_meeting_ids": [1],
- "option_ids": [1],
- "poll_candidate_ids": [1],
- "poll_voted_ids": [1],
- "vote_ids": [1],
- "delegated_vote_ids": [2],
- },
- "meeting/1": {
- "present_user_ids": [3],
- },
- "poll/1": {"voted_ids": [3]},
- "option/1": {"content_object_id": "user/3"},
- "poll_candidate/1": {
- "user_id": 3,
- },
- "vote/1": {"user_id": 3},
- "vote/2": {"delegated_user_id": 3},
- }
- for fqid, model in expected.items():
- self.assert_model_exists(fqid, model)
+ self.assert_model_exists("user/2", {"is_present_in_meeting_ids": [1]})
+ self.assert_model_exists("meeting/1", {"present_user_ids": [2]})
def test_update_with_internal_fields_error(self) -> None:
self.create_meeting()
- self.create_user("dummy2", [1])
- self.create_user("dummy3", [1])
+ self.create_user_for_meeting(1)
response = self.request(
"user.update",
{
- "id": 3,
+ "id": 2,
"is_present_in_meeting_ids": [1],
- "option_ids": [1],
- "poll_candidate_ids": [1],
- "poll_voted_ids": [1],
- "vote_ids": [1],
- "delegated_vote_ids": [2],
},
internal=False,
)
self.assert_status_code(response, 400)
- message: str = response.json["message"]
- assert message.startswith("data must not contain {")
- assert message.endswith("} properties")
- for field in [
- "'is_present_in_meeting_ids'",
- "'option_ids'",
- "'poll_candidate_ids'",
- "'poll_voted_ids'",
- "'vote_ids'",
- "'delegated_vote_ids'",
- ]:
- self.assertIn(field, message)
+ self.assertEqual(
+ "data must not contain {'is_present_in_meeting_ids'} properties",
+ response.json["message"],
+ )
def test_update_groups_on_last_meeting_admin(self) -> None:
self.create_meeting()
diff --git a/tests/system/action/vote/__init__.py b/tests/system/action/vote/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/tests/system/action/vote/test_create.py b/tests/system/action/vote/test_create.py
deleted file mode 100644
index 8ae1dc7ae3..0000000000
--- a/tests/system/action/vote/test_create.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from decimal import Decimal
-
-from tests.system.action.base import BaseActionTestCase
-
-
-class VoteCreateActionTest(BaseActionTestCase):
- def test_create(self) -> None:
- self.create_meeting(111)
- self.set_models({"option/12": {"text": "blabalbal", "meeting_id": 111}})
- response = self.request(
- "vote.create",
- {
- "value": "Y",
- "weight": "1.000000",
- "option_id": 12,
- "user_token": "aaaabbbbccccdddd",
- },
- )
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "vote/1",
- {
- "value": "Y",
- "meeting_id": 111,
- "weight": Decimal("1.000000"),
- "option_id": 12,
- "user_token": "aaaabbbbccccdddd",
- },
- )
- self.assert_model_exists("option/12", {"vote_ids": [1]})
diff --git a/tests/system/action/vote/test_delete.py b/tests/system/action/vote/test_delete.py
deleted file mode 100644
index 48f7d86869..0000000000
--- a/tests/system/action/vote/test_delete.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from tests.system.action.base import BaseActionTestCase
-
-
-class VoteDeleteTest(BaseActionTestCase):
- def setUp(self) -> None:
- super().setUp()
- self.create_meeting()
- self.set_models(
- {
- "option/12": {"text": "blabalbal", "meeting_id": 1},
- "vote/111": {
- "meeting_id": 1,
- "option_id": 12,
- "user_token": "aaaabbbbccccdddd",
- },
- }
- )
-
- def test_delete_correct(self) -> None:
- response = self.request("vote.delete", {"id": 111})
- self.assert_status_code(response, 200)
- self.assert_model_not_exists("vote/111")
-
- def test_delete_wrong_id(self) -> None:
- response = self.request("vote.delete", {"id": 112})
- self.assert_status_code(response, 400)
- self.assert_model_exists("vote/111")
diff --git a/tests/system/action/vote/test_update.py b/tests/system/action/vote/test_update.py
deleted file mode 100644
index 5f757c2cb4..0000000000
--- a/tests/system/action/vote/test_update.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from decimal import Decimal
-
-from tests.system.action.base import BaseActionTestCase
-
-
-class VoteUpdateActionTest(BaseActionTestCase):
- def test_update(self) -> None:
- self.create_meeting(111)
- self.set_models(
- {
- "option/12": {"text": "blabalbal", "meeting_id": 111},
- "vote/1": {
- "value": "Y",
- "meeting_id": 111,
- "weight": "1.000000",
- "option_id": 12,
- "user_token": "aaaabbbbccccdddd",
- },
- }
- )
-
- response = self.request("vote.update", {"id": 1, "weight": "1.500000"})
- self.assert_status_code(response, 200)
- self.assert_model_exists(
- "vote/1", {"value": "Y", "meeting_id": 111, "weight": Decimal("1.500000")}
- )
diff --git a/tests/system/presenter/test_check_database.py b/tests/system/presenter/test_check_database.py
index 087f0120ce..ac68dfef21 100644
--- a/tests/system/presenter/test_check_database.py
+++ b/tests/system/presenter/test_check_database.py
@@ -2,7 +2,7 @@
from psycopg.types.json import Jsonb
-from openslides_backend.models.models import Meeting
+from openslides_backend.models.models import Meeting, Poll
from openslides_backend.permissions.management_levels import OrganizationManagementLevel
from openslides_backend.shared.util import ONE_ORGANIZATION_FQID, ONE_ORGANIZATION_ID
@@ -112,8 +112,8 @@ def get_meeting_defaults(self) -> dict[str, Any]:
"assignment_poll_default_backend": "fast",
"poll_default_type": "analog",
"poll_default_onehundred_percent_base": "YNA",
- "poll_default_backend": "fast",
"poll_default_live_voting_enabled": False,
+ "poll_default_allow_invalid": False,
"poll_couple_countdown": True,
}
@@ -284,8 +284,6 @@ def test_correct_relations(self) -> None:
"motion_ids": [1],
"motion_submitter_ids": [5],
"list_of_speakers_ids": [6, 11],
- "vote_ids": [7],
- "option_ids": [8],
"assignment_candidate_ids": [9],
"assignment_ids": [10],
# relation fields.
@@ -342,17 +340,11 @@ def test_correct_relations(self) -> None:
),
"user/4": self.get_new_user(
"vote_user",
- {
- "meeting_user_ids": [14],
- "vote_ids": [7],
- },
+ {"meeting_user_ids": [14]},
),
"user/5": self.get_new_user(
"delegated_user",
- {
- "meeting_user_ids": [15],
- "delegated_vote_ids": [7],
- },
+ {"meeting_user_ids": [15]},
),
"user/6": self.get_new_user(
"candidate_user",
@@ -385,11 +377,13 @@ def test_correct_relations(self) -> None:
"user_id": 4,
"meeting_id": 1,
"group_ids": [1],
+ "acting_ballot_ids": [7],
},
"meeting_user/15": {
"user_id": 5,
"meeting_id": 1,
"group_ids": [1],
+ "represented_ballot_ids": [7],
},
"meeting_user/16": {
"user_id": 6,
@@ -498,17 +492,19 @@ def test_correct_relations(self) -> None:
"content_object_id": "motion/1",
"meeting_id": 1,
},
- "vote/7": {
- "user_token": "test",
- "option_id": 8,
- "user_id": 4,
- "delegated_user_id": 5,
+ "poll/7": {
+ "title": "Poll 7",
"meeting_id": 1,
- },
- "option/8": {
- "vote_ids": [7],
- "meeting_id": 1,
- "weight": 10000,
+ "content_object_id": "motion/1",
+ "visibility": Poll.VISIBILITY_NAMED,
+ "config_id": "poll_config_rating_approval/1",
+ "state": Poll.STATE_STARTED,
+ },
+ "poll_config_rating_approval/7": {"poll_id": 7},
+ "ballot/8": {
+ "poll_id": 7,
+ "acting_meeting_user_id": 14,
+ "represented_meeting_user_id": 15,
},
"assignment_candidate/9": {
"weight": 10000,
diff --git a/tests/system/presenter/test_check_database_all.py b/tests/system/presenter/test_check_database_all.py
index f5a174e8fd..1edc50e80d 100644
--- a/tests/system/presenter/test_check_database_all.py
+++ b/tests/system/presenter/test_check_database_all.py
@@ -2,7 +2,7 @@
from typing import Any
from openslides_backend.action.action_worker import ActionWorkerState
-from openslides_backend.models.models import Meeting
+from openslides_backend.models.models import Meeting, Poll
from openslides_backend.permissions.management_levels import OrganizationManagementLevel
from openslides_backend.shared.util import ONE_ORGANIZATION_FQID, ONE_ORGANIZATION_ID
@@ -119,8 +119,8 @@ def get_meeting_defaults(self) -> dict[str, Any]:
"assignment_poll_default_backend": "fast",
"poll_default_type": "analog",
"poll_default_onehundred_percent_base": "YNA",
- "poll_default_backend": "fast",
"poll_default_live_voting_enabled": False,
+ "poll_default_allow_invalid": False,
"poll_couple_countdown": True,
}
@@ -376,8 +376,6 @@ def test_correct_relations(self) -> None:
"motion_ids": [1],
"motion_submitter_ids": [5],
"list_of_speakers_ids": [6, 11],
- "vote_ids": [7],
- "option_ids": [8],
"assignment_candidate_ids": [9],
"assignment_ids": [10],
# relation fields.
@@ -433,14 +431,14 @@ def test_correct_relations(self) -> None:
),
"user/4": self.get_new_user(
"vote_user",
- {"meeting_user_ids": [14], "vote_ids": [7], "is_active": False},
+ {
+ "meeting_user_ids": [14],
+ "is_active": False,
+ },
),
"user/5": self.get_new_user(
"delegated_user",
- {
- "meeting_user_ids": [15],
- "delegated_vote_ids": [7],
- },
+ {"meeting_user_ids": [15]},
),
"user/6": self.get_new_user(
"candidate_user",
@@ -468,11 +466,13 @@ def test_correct_relations(self) -> None:
"meeting_id": 1,
"user_id": 4,
"group_ids": [1],
+ "acting_ballot_ids": [7],
},
"meeting_user/15": {
"meeting_id": 1,
"user_id": 5,
"group_ids": [1],
+ "represented_ballot_ids": [7],
},
"meeting_user/16": {
"meeting_id": 1,
@@ -566,17 +566,19 @@ def test_correct_relations(self) -> None:
"content_object_id": "motion/1",
"meeting_id": 1,
},
- "vote/7": {
- "user_token": "test",
- "option_id": 8,
- "user_id": 4,
- "delegated_user_id": 5,
- "meeting_id": 1,
- },
- "option/8": {
- "vote_ids": [7],
+ "poll/7": {
+ "title": "Poll 1",
"meeting_id": 1,
- "weight": 10000,
+ "content_object_id": "motion/1",
+ "visibility": Poll.VISIBILITY_NAMED,
+ "config_id": "poll_config_rating_approval/1",
+ "state": Poll.STATE_STARTED,
+ },
+ "poll_config_rating_approval/7": {"poll_id": 7},
+ "ballot/8": {
+ "poll_id": 7,
+ "acting_meeting_user_id": 14,
+ "represented_meeting_user_id": 15,
},
"assignment_candidate/9": {
"weight": 10000,
diff --git a/tests/system/presenter/test_export_meeting.py b/tests/system/presenter/test_export_meeting.py
index cadbde81fc..e7df63f681 100644
--- a/tests/system/presenter/test_export_meeting.py
+++ b/tests/system/presenter/test_export_meeting.py
@@ -61,7 +61,6 @@ def test_correct(self) -> None:
"motion_block",
"motion_change_recommendation",
"poll",
- "option",
"vote",
"assignment",
"assignment_candidate",
@@ -217,12 +216,18 @@ def test_export_meeting_with_ex_user(self) -> None:
def test_export_meeting_find_special_users(self) -> None:
"""Find users in:
- Collection | Field
- meeting | present_user_ids
- motion | supporter_meeting_user_ids
- poll | voted_ids
- vote | delegated_meeting_user_id
+ Collection | Field
+ meeting | present_user_ids
+
+ Find meeting_users in:
+ Collection | Field
+ poll | voted_ids
+ motion | supporter_meeting_user_ids
+ ballot | acting_meeting_user_id
+ ballot | represented_meeting_user_id
+ poll_config_option | meeting_user_id
"""
+ self.create_meeting()
self.create_motion(1, 30)
self.set_models(
{
@@ -232,45 +237,59 @@ def test_export_meeting_find_special_users(self) -> None:
"user/12": {"username": "exuser12"},
"user/13": {"username": "exuser13"},
"user/14": {"username": "exuser14"},
- "meeting_user/112": {
- "meeting_id": 1,
- "user_id": 12,
- },
+ "meeting_user/112": {"meeting_id": 1, "user_id": 12},
"motion_supporter/1": {
"motion_id": 30,
"meeting_id": 1,
"meeting_user_id": 112,
},
- "meeting_user/114": {
- "meeting_id": 1,
- "user_id": 14,
- },
+ "meeting_user/113": {"meeting_id": 1, "user_id": 13},
+ "meeting_user/114": {"meeting_id": 1, "user_id": 14},
"poll/80": {
"title": "Poll 80",
- "type": Poll.TYPE_NAMED,
- "backend": "fast",
- "pollmethod": "YNA",
- "onehundred_percent_base": "YNA",
"meeting_id": 1,
- "content_object_id": "motion/30",
- "state": Poll.STATE_PUBLISHED,
- "voted_ids": [13],
+ "content_object_id": "assignment/10",
+ "visibility": Poll.VISIBILITY_NAMED,
+ "config_id": "poll_config_approval/90",
+ "state": Poll.STATE_STARTED,
+ "voted_ids": [114],
},
- "vote/120": {
- "meeting_id": 1,
- "delegated_user_id": 14,
- "user_id": 14,
- "user_token": "asdfgh",
- "option_id": 1,
+ "poll_config_approval/90": {"poll_id": 80},
+ "poll_config_option/100": {
+ "poll_config_id": "poll_config_approval/90",
+ "meeting_user_id": 113,
+ },
+ "ballot/120": {
+ "poll_id": 80,
+ "value": "yes",
+ "represented_meeting_user_id": 114,
+ "acting_meeting_user_id": 114,
},
- "option/1": {"meeting_id": 1},
}
)
status_code, data = self.request("export_meeting", {"meeting_id": 1})
assert status_code == 200
assert data["meeting"]["1"].get("user_ids") is None
- for id_ in ("11", "12", "13", "14"):
- assert data["user"][id_]
+ for id_ in range(11, 15):
+ assert data["user"][str(id_)]
+ for id_ in range(112, 115):
+ assert data["meeting_user"][str(id_)]
+
+ assert data["meeting"]["1"]["present_user_ids"] == [11]
+ assert data["user"]["11"]["is_present_in_meeting_ids"] == [1]
+
+ assert data["motion"]["30"]["supporter_meeting_user_ids"] == [112]
+ assert data["meeting_user"]["112"]["supported_motion_ids"] == [30]
+
+ assert data["poll_config_option"]["100"]["meeting_user_id"] == 113
+ assert data["meeting_user"]["113"]["poll_option_ids"] == [100]
+
+ assert data["poll"]["80"]["voted_ids"] == [114]
+ assert data["meeting_user"]["114"]["poll_voted_ids"] == [80]
+ assert data["ballot"]["120"]["c"] == 114
+ assert data["ballot"]["120"]["represented_meeting_user_id"] == 114
+ assert data["meeting_user"]["114"]["acting_ballot_ids"] == [120]
+ assert data["meeting_user"]["114"]["represented_ballot_ids"] == [120]
def test_with_structured_published_orga_files(self) -> None:
self.set_models(
@@ -385,7 +404,6 @@ def test_with_structured_published_orga_files(self) -> None:
"assignment_poll_default_backend": "fast",
"poll_default_type": "analog",
"poll_default_onehundred_percent_base": "YNA",
- "poll_default_backend": "fast",
"poll_default_live_voting_enabled": False,
"poll_couple_countdown": True,
},
diff --git a/tests/system/util.py b/tests/system/util.py
index 896666a6bc..6fed3c97b8 100644
--- a/tests/system/util.py
+++ b/tests/system/util.py
@@ -1,4 +1,3 @@
-import copy
import cProfile
import os
from abc import abstractmethod
@@ -39,18 +38,13 @@ class TestVoteService(VoteService):
url: str
@abstractmethod
- def vote(self, data: dict[str, Any]) -> Response: ...
+ def vote(self, poll_id: int, payload: dict[str, Any]) -> Response: ...
class TestVoteAdapter(VoteAdapter, TestVoteService):
- def vote(self, data: dict[str, Any]) -> Response:
- data_copy = copy.deepcopy(data)
- del data_copy["id"]
- response = self.make_request(
- self.url.replace("internal", "system") + f"?id={data['id']}",
- data_copy,
- )
- return convert_to_test_response(response)
+ def vote(self, poll_id: int, payload: dict[str, Any]) -> Response:
+ endpoint = self.url + f"?id={poll_id}"
+ return self.retrieve(endpoint, payload)
def create_action_test_application() -> OpenSlidesBackendWSGIApplication:
From e7a5dc88991cae4b80fec9bca0b49e93e5e9df7e Mon Sep 17 00:00:00 2001
From: Bastian Rihm
Date: Mon, 2 Mar 2026 12:07:25 +0100
Subject: [PATCH 02/23] Update vote meta (#3376)
---
meta | 2 +-
openslides_backend/models/models.py | 20 +++++++++++++++-----
2 files changed, 16 insertions(+), 6 deletions(-)
diff --git a/meta b/meta
index 11fb92a1f9..95f672cf08 160000
--- a/meta
+++ b/meta
@@ -1 +1 @@
-Subproject commit 11fb92a1f9722f479e419395a7aacab075a347ab
+Subproject commit 95f672cf080b19c9fdd83d717204af9855c5ccfe
diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py
index 0923f70dfa..ce285f7744 100644
--- a/openslides_backend/models/models.py
+++ b/openslides_backend/models/models.py
@@ -2524,7 +2524,9 @@ class PollConfigApproval(Model):
verbose_name = "poll config approval"
id = fields.IntegerField(required=True, constant=True)
- poll_id = fields.RelationField(to={"poll": "config_id"}, required=True)
+ poll_id = fields.RelationField(
+ to={"poll": "config_id"}, is_view_field=True, required=True
+ )
option_ids = fields.RelationListField(
to={"poll_config_option": "poll_config_id"},
on_delete=fields.OnDelete.CASCADE,
@@ -2558,7 +2560,9 @@ class PollConfigRatingApproval(Model):
verbose_name = "poll config rating approval"
id = fields.IntegerField(required=True, constant=True)
- poll_id = fields.RelationField(to={"poll": "config_id"}, required=True)
+ poll_id = fields.RelationField(
+ to={"poll": "config_id"}, is_view_field=True, required=True
+ )
option_ids = fields.RelationListField(
to={"poll_config_option": "poll_config_id"},
on_delete=fields.OnDelete.CASCADE,
@@ -2574,7 +2578,9 @@ class PollConfigRatingScore(Model):
verbose_name = "poll config rating score"
id = fields.IntegerField(required=True, constant=True)
- poll_id = fields.RelationField(to={"poll": "config_id"}, required=True)
+ poll_id = fields.RelationField(
+ to={"poll": "config_id"}, is_view_field=True, required=True
+ )
option_ids = fields.RelationListField(
to={"poll_config_option": "poll_config_id"},
on_delete=fields.OnDelete.CASCADE,
@@ -2592,7 +2598,9 @@ class PollConfigSelection(Model):
verbose_name = "poll config selection"
id = fields.IntegerField(required=True, constant=True)
- poll_id = fields.RelationField(to={"poll": "config_id"}, required=True)
+ poll_id = fields.RelationField(
+ to={"poll": "config_id"}, is_view_field=True, required=True
+ )
option_ids = fields.RelationListField(
to={"poll_config_option": "poll_config_id"},
on_delete=fields.OnDelete.CASCADE,
@@ -2608,7 +2616,9 @@ class PollConfigStvScottish(Model):
verbose_name = "poll config stv scottish"
id = fields.IntegerField(required=True, constant=True)
- poll_id = fields.RelationField(to={"poll": "config_id"}, required=True)
+ poll_id = fields.RelationField(
+ to={"poll": "config_id"}, is_view_field=True, required=True
+ )
option_ids = fields.RelationListField(
to={"poll_config_option": "poll_config_id"},
on_delete=fields.OnDelete.CASCADE,
From a2fc58267faacc4d3bcea562407029908dee7847 Mon Sep 17 00:00:00 2001
From: Bastian Rihm
Date: Fri, 20 Mar 2026 13:03:28 +0100
Subject: [PATCH 03/23] Add onehundred percent base fields (#3386)
---
data/example-data.json | 28 +++---
meta | 2 +-
.../action/actions/meeting/update.py | 4 +-
openslides_backend/models/models.py | 85 ++++++++++++++-----
4 files changed, 80 insertions(+), 39 deletions(-)
diff --git a/data/example-data.json b/data/example-data.json
index e2ea9a0822..f8130ed2be 100644
--- a/data/example-data.json
+++ b/data/example-data.json
@@ -355,14 +355,13 @@
"motions_export_follow_recommendation": false,
"motion_poll_ballot_paper_selection": "CUSTOM_NUMBER",
"motion_poll_ballot_paper_number": 8,
- "motion_poll_default_type": "pseudoanonymous",
- "motion_poll_default_method": "YNA",
- "motion_poll_default_onehundred_percent_base": "YNA",
+ "motion_poll_default_type": "secret",
+ "motion_poll_default_allow_abstain": true,
+ "motion_poll_default_onehundred_percent_base": "valid",
"motion_poll_default_group_ids": [
2,
3
],
- "motion_poll_default_backend": "fast",
"motion_poll_projection_name_order_first": "last_name",
"motion_poll_projection_max_columns": 6,
"users_enable_presence_view": true,
@@ -381,14 +380,13 @@
"assignment_poll_add_candidates_to_list_of_speakers": false,
"assignment_poll_enable_max_votes_per_option": false,
"assignment_poll_sort_poll_result_by_votes": true,
- "assignment_poll_default_type": "pseudoanonymous",
- "assignment_poll_default_method": "Y",
+ "assignment_poll_default_type": "secret",
+ "assignment_poll_default_method": "selection",
"assignment_poll_default_onehundred_percent_base": "valid",
"assignment_poll_default_group_ids": [
2,
3
],
- "assignment_poll_default_backend": "fast",
"poll_ballot_paper_selection": "CUSTOM_NUMBER",
"poll_ballot_paper_number": 8,
"poll_sort_poll_result_by_votes": true,
@@ -1965,13 +1963,15 @@
"1": {
"id": 1,
"poll_id": 1,
- "allow_abstain": false
+ "allow_abstain": false,
+ "onehundred_percent_base": "valid"
},
"2": {
"id": 2,
"poll_id": 2,
"allow_abstain": true,
- "option_ids": [1, 2]
+ "option_ids": [1, 2],
+ "onehundred_percent_base": "valid"
}
},
"poll_config_selection": {
@@ -1981,7 +1981,9 @@
"option_ids": [3, 4, 5],
"max_options_amount": 2,
"min_options_amount": 1,
- "allow_nota": true
+ "allow_nota": true,
+ "strike_out": false,
+ "onehundred_percent_base": "valid"
}
},
"poll_config_rating_score": {
@@ -1993,7 +1995,8 @@
"min_options_amount": 2,
"max_votes_per_option": 0,
"max_vote_sum": 5,
- "min_vote_sum": 1
+ "min_vote_sum": 1,
+ "onehundred_percent_base": "valid"
}
},
"poll_config_rating_approval": {
@@ -2003,7 +2006,8 @@
"max_options_amount": 0,
"min_options_amount": 0,
"allow_abstain": true,
- "option_ids": [8, 9, 10]
+ "option_ids": [8, 9, 10],
+ "onehundred_percent_base": "valid"
}
},
"poll_config_option": {
diff --git a/meta b/meta
index 95f672cf08..594c08ae41 160000
--- a/meta
+++ b/meta
@@ -1 +1 @@
-Subproject commit 95f672cf080b19c9fdd83d717204af9855c5ccfe
+Subproject commit 594c08ae412d572e42ca9ba534c41f139f2dd8cb
diff --git a/openslides_backend/action/actions/meeting/update.py b/openslides_backend/action/actions/meeting/update.py
index 6a798ac720..a82fb6f852 100644
--- a/openslides_backend/action/actions/meeting/update.py
+++ b/openslides_backend/action/actions/meeting/update.py
@@ -134,11 +134,10 @@
"motions_enable_restricted_editor_for_non_manager",
"motion_poll_ballot_paper_selection",
"motion_poll_ballot_paper_number",
+ "motion_poll_default_allow_abstain",
"motion_poll_default_type",
- "motion_poll_default_method",
"motion_poll_default_onehundred_percent_base",
"motion_poll_default_group_ids",
- "motion_poll_default_backend",
"motion_poll_projection_name_order_first",
"motion_poll_projection_max_columns",
"users_enable_presence_view",
@@ -169,7 +168,6 @@
"assignment_poll_default_method",
"assignment_poll_default_onehundred_percent_base",
"assignment_poll_default_group_ids",
- "assignment_poll_default_backend",
"topic_poll_default_group_ids",
"poll_default_live_voting_enabled",
"poll_default_allow_invalid",
diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py
index ce285f7744..9d3e1b6c51 100644
--- a/openslides_backend/models/models.py
+++ b/openslides_backend/models/models.py
@@ -918,16 +918,13 @@ class Meeting(Model, MeetingModelMixin):
},
)
motion_poll_ballot_paper_number = fields.IntegerField(default=8)
- motion_poll_default_type = fields.CharField(default="pseudoanonymous")
- motion_poll_default_method = fields.CharField(default="YNA")
+ motion_poll_default_type = fields.CharField(default="secret")
+ motion_poll_default_allow_abstain = fields.BooleanField(default=True)
motion_poll_default_onehundred_percent_base = fields.CharField(
- default="YNA",
+ default="valid",
constraints={
"enum": [
- "Y",
- "YN",
- "YNA",
- "N",
+ "yes_no",
"valid",
"cast",
"entitled",
@@ -939,9 +936,6 @@ class Meeting(Model, MeetingModelMixin):
motion_poll_default_group_ids = fields.RelationListField(
to={"group": "used_as_motion_poll_default_id"}, is_view_field=True
)
- motion_poll_default_backend = fields.CharField(
- default="fast", constraints={"enum": ["long", "fast"]}
- )
motion_poll_projection_name_order_first = fields.CharField(
required=True,
default="last_name",
@@ -994,16 +988,14 @@ class Meeting(Model, MeetingModelMixin):
)
assignment_poll_enable_max_votes_per_option = fields.BooleanField(default=False)
assignment_poll_sort_poll_result_by_votes = fields.BooleanField(default=True)
- assignment_poll_default_type = fields.CharField(default="pseudoanonymous")
- assignment_poll_default_method = fields.CharField(default="Y")
+ assignment_poll_default_type = fields.CharField(default="secret")
+ assignment_poll_default_method = fields.CharField(default="selection")
assignment_poll_default_onehundred_percent_base = fields.CharField(
default="valid",
constraints={
"enum": [
- "Y",
- "YN",
- "YNA",
- "N",
+ "no_general",
+ "yes_no",
"valid",
"cast",
"entitled",
@@ -1015,9 +1007,6 @@ class Meeting(Model, MeetingModelMixin):
assignment_poll_default_group_ids = fields.RelationListField(
to={"group": "used_as_assignment_poll_default_id"}, is_view_field=True
)
- assignment_poll_default_backend = fields.CharField(
- default="fast", constraints={"enum": ["long", "fast"]}
- )
poll_ballot_paper_selection = fields.CharField(
constraints={
"enum": [
@@ -1035,10 +1024,7 @@ class Meeting(Model, MeetingModelMixin):
default="YNA",
constraints={
"enum": [
- "Y",
- "YN",
- "YNA",
- "N",
+ "no_general",
"valid",
"cast",
"entitled",
@@ -2533,6 +2519,19 @@ class PollConfigApproval(Model):
is_view_field=True,
)
allow_abstain = fields.BooleanField(default=True)
+ onehundred_percent_base = fields.CharField(
+ required=True,
+ constraints={
+ "enum": [
+ "yes_no",
+ "valid",
+ "cast",
+ "entitled",
+ "entitled_present",
+ "disabled",
+ ]
+ },
+ )
class PollConfigOption(Model):
@@ -2571,6 +2570,19 @@ class PollConfigRatingApproval(Model):
max_options_amount = fields.IntegerField(default=0)
min_options_amount = fields.IntegerField(default=0)
allow_abstain = fields.BooleanField(default=True)
+ onehundred_percent_base = fields.CharField(
+ required=True,
+ constraints={
+ "enum": [
+ "yes_no",
+ "valid",
+ "cast",
+ "entitled",
+ "entitled_present",
+ "disabled",
+ ]
+ },
+ )
class PollConfigRatingScore(Model):
@@ -2591,6 +2603,19 @@ class PollConfigRatingScore(Model):
max_votes_per_option = fields.IntegerField(default=0)
max_vote_sum = fields.IntegerField(default=0)
min_vote_sum = fields.IntegerField(default=0)
+ onehundred_percent_base = fields.CharField(
+ required=True,
+ constraints={
+ "enum": [
+ "yes_no",
+ "valid",
+ "cast",
+ "entitled",
+ "entitled_present",
+ "disabled",
+ ]
+ },
+ )
class PollConfigSelection(Model):
@@ -2609,6 +2634,20 @@ class PollConfigSelection(Model):
max_options_amount = fields.IntegerField(default=0)
min_options_amount = fields.IntegerField(default=0)
allow_nota = fields.BooleanField(default=False)
+ strike_out = fields.BooleanField(default=False)
+ onehundred_percent_base = fields.CharField(
+ required=True,
+ constraints={
+ "enum": [
+ "no_general",
+ "valid",
+ "cast",
+ "entitled",
+ "entitled_present",
+ "disabled",
+ ]
+ },
+ )
class PollConfigStvScottish(Model):
From 143ab4b68ed839c181cd89ea48a2d29427789df0 Mon Sep 17 00:00:00 2001
From: Bastian Rihm
Date: Fri, 20 Mar 2026 13:18:12 +0100
Subject: [PATCH 04/23] Update poll options format
---
data/example-data.json | 30 ++++-----
meta | 2 +-
.../action/actions/user/merge_mixins.py | 8 +--
openslides_backend/models/models.py | 67 +++++--------------
4 files changed, 38 insertions(+), 69 deletions(-)
diff --git a/data/example-data.json b/data/example-data.json
index f8130ed2be..6286e36385 100644
--- a/data/example-data.json
+++ b/data/example-data.json
@@ -1905,6 +1905,7 @@
"title": "2",
"config_id": "poll_config_approval/2",
"visibility": "manually",
+ "option_ids": [1, 2],
"state": "finished",
"published": true,
"allow_invalid": false,
@@ -1917,6 +1918,7 @@
"title": "3",
"config_id": "poll_config_selection/1",
"visibility": "open",
+ "option_ids": [3, 4, 5],
"state": "started",
"published": false,
"allow_invalid": false,
@@ -1931,6 +1933,7 @@
"title": "4",
"config_id": "poll_config_rating_score/1",
"visibility": "secret",
+ "option_ids": [6, 7],
"state": "created",
"published": false,
"allow_invalid": false,
@@ -1943,6 +1946,7 @@
"title": "5",
"config_id": "poll_config_rating_approval/1",
"visibility": "manually",
+ "option_ids": [8, 9, 10],
"state": "finished",
"published": false,
"allow_invalid": false,
@@ -1970,7 +1974,6 @@
"id": 2,
"poll_id": 2,
"allow_abstain": true,
- "option_ids": [1, 2],
"onehundred_percent_base": "valid"
}
},
@@ -1978,7 +1981,6 @@
"1": {
"id": 1,
"poll_id": 3,
- "option_ids": [3, 4, 5],
"max_options_amount": 2,
"min_options_amount": 1,
"allow_nota": true,
@@ -1990,7 +1992,6 @@
"1": {
"id": 1,
"poll_id": 4,
- "option_ids": [6, 7],
"max_options_amount": 2,
"min_options_amount": 2,
"max_votes_per_option": 0,
@@ -2006,61 +2007,60 @@
"max_options_amount": 0,
"min_options_amount": 0,
"allow_abstain": true,
- "option_ids": [8, 9, 10],
"onehundred_percent_base": "valid"
}
},
- "poll_config_option": {
+ "poll_option": {
"1": {
"id": 1,
"weight": 1,
- "poll_config_id": "poll_config_approval/2",
+ "poll_id": 2,
"meeting_user_id": 1
},
"2": {
"id": 2,
"weight": 2,
- "poll_config_id": "poll_config_approval/2",
+ "poll_id": 2,
"meeting_user_id": 3
},
"3": {
"id": 3,
- "poll_config_id": "poll_config_selection/1",
+ "poll_id": 3,
"text": "Option 3"
},
"4": {
"id": 4,
- "poll_config_id": "poll_config_selection/1",
+ "poll_id": 3,
"text": "Option 4"
},
"5": {
"id": 5,
- "poll_config_id": "poll_config_selection/1",
+ "poll_id": 3,
"text": "Option 5"
},
"6": {
"id": 6,
- "poll_config_id": "poll_config_rating_score/1",
+ "poll_id": 4,
"text": "option 6"
},
"7": {
"id": 7,
- "poll_config_id": "poll_config_rating_score/1",
+ "poll_id": 4,
"text": "option 7"
},
"8": {
"id": 8,
- "poll_config_id": "poll_config_rating_approval/1",
+ "poll_id": 5,
"meeting_user_id": 1
},
"9": {
"id": 9,
- "poll_config_id": "poll_config_rating_approval/1",
+ "poll_id": 5,
"meeting_user_id": 2
},
"10": {
"id": 10,
- "poll_config_id": "poll_config_rating_approval/1",
+ "poll_id": 5,
"meeting_user_id": 3
}
},
diff --git a/meta b/meta
index 594c08ae41..98929842c8 160000
--- a/meta
+++ b/meta
@@ -1 +1 @@
-Subproject commit 594c08ae412d572e42ca9ba534c41f139f2dd8cb
+Subproject commit 98929842c8aaaaed2f381e177717d07fe23625ac
diff --git a/openslides_backend/action/actions/user/merge_mixins.py b/openslides_backend/action/actions/user/merge_mixins.py
index 23c216164d..b73dad7f0f 100644
--- a/openslides_backend/action/actions/user/merge_mixins.py
+++ b/openslides_backend/action/actions/user/merge_mixins.py
@@ -12,7 +12,7 @@
MotionWorkingGroupSpeaker,
PersonalNote,
Poll,
- PollConfigOption,
+ PollOption,
Speaker,
)
from ....shared.exceptions import ActionException
@@ -207,11 +207,11 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
)
-class PollConfigOptionMergeMixin(BaseMergeMixin):
+class PollOptionMergeMixin(BaseMergeMixin):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.add_collection_field_groups(
- PollConfigOption,
+ PollOption,
{},
"meeting_user_id",
)
@@ -235,7 +235,7 @@ class MeetingUserMergeMixin(
MotionSupporterMergeMixin,
AssignmentCandidateMergeMixin,
PollMergeMixin,
- PollConfigOptionMergeMixin,
+ PollOptionMergeMixin,
BallotMergeMixin,
SpeakerMergeMixin,
):
diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py
index 9d3e1b6c51..01b818a7fd 100644
--- a/openslides_backend/models/models.py
+++ b/openslides_backend/models/models.py
@@ -1021,7 +1021,7 @@ class Meeting(Model, MeetingModelMixin):
poll_default_type = fields.CharField(default="analog")
poll_default_method = fields.CharField()
poll_default_onehundred_percent_base = fields.CharField(
- default="YNA",
+ default="valid",
constraints={
"enum": [
"no_general",
@@ -1599,9 +1599,7 @@ class MeetingUser(Model):
),
)
poll_option_ids = fields.RelationListField(
- to={"poll_config_option": "meeting_user_id"},
- is_view_field=True,
- is_primary=True,
+ to={"poll_option": "meeting_user_id"}, is_view_field=True, is_primary=True
)
acting_ballot_ids = fields.RelationListField(
to={"ballot": "acting_meeting_user_id"}, is_view_field=True
@@ -2432,6 +2430,11 @@ class Poll(Model, PollModelMixin):
},
required=True,
)
+ option_ids = fields.RelationListField(
+ to={"poll_option": "poll_id"},
+ on_delete=fields.OnDelete.CASCADE,
+ is_view_field=True,
+ )
visibility = fields.CharField(
required=True, constraints={"enum": ["manually", "named", "open", "secret"]}
)
@@ -2513,11 +2516,6 @@ class PollConfigApproval(Model):
poll_id = fields.RelationField(
to={"poll": "config_id"}, is_view_field=True, required=True
)
- option_ids = fields.RelationListField(
- to={"poll_config_option": "poll_config_id"},
- on_delete=fields.OnDelete.CASCADE,
- is_view_field=True,
- )
allow_abstain = fields.BooleanField(default=True)
onehundred_percent_base = fields.CharField(
required=True,
@@ -2534,26 +2532,6 @@ class PollConfigApproval(Model):
)
-class PollConfigOption(Model):
- collection = "poll_config_option"
- verbose_name = "poll config option"
-
- id = fields.IntegerField(required=True, constant=True)
- poll_config_id = fields.GenericRelationField(
- to={
- "poll_config_stv_scottish": "option_ids",
- "poll_config_rating_approval": "option_ids",
- "poll_config_rating_score": "option_ids",
- "poll_config_selection": "option_ids",
- "poll_config_approval": "option_ids",
- },
- required=True,
- )
- weight = fields.IntegerField()
- text = fields.CharField()
- meeting_user_id = fields.RelationField(to={"meeting_user": "poll_option_ids"})
-
-
class PollConfigRatingApproval(Model):
collection = "poll_config_rating_approval"
verbose_name = "poll config rating approval"
@@ -2562,11 +2540,6 @@ class PollConfigRatingApproval(Model):
poll_id = fields.RelationField(
to={"poll": "config_id"}, is_view_field=True, required=True
)
- option_ids = fields.RelationListField(
- to={"poll_config_option": "poll_config_id"},
- on_delete=fields.OnDelete.CASCADE,
- is_view_field=True,
- )
max_options_amount = fields.IntegerField(default=0)
min_options_amount = fields.IntegerField(default=0)
allow_abstain = fields.BooleanField(default=True)
@@ -2593,11 +2566,6 @@ class PollConfigRatingScore(Model):
poll_id = fields.RelationField(
to={"poll": "config_id"}, is_view_field=True, required=True
)
- option_ids = fields.RelationListField(
- to={"poll_config_option": "poll_config_id"},
- on_delete=fields.OnDelete.CASCADE,
- is_view_field=True,
- )
max_options_amount = fields.IntegerField(default=0)
min_options_amount = fields.IntegerField(default=0)
max_votes_per_option = fields.IntegerField(default=0)
@@ -2626,11 +2594,6 @@ class PollConfigSelection(Model):
poll_id = fields.RelationField(
to={"poll": "config_id"}, is_view_field=True, required=True
)
- option_ids = fields.RelationListField(
- to={"poll_config_option": "poll_config_id"},
- on_delete=fields.OnDelete.CASCADE,
- is_view_field=True,
- )
max_options_amount = fields.IntegerField(default=0)
min_options_amount = fields.IntegerField(default=0)
allow_nota = fields.BooleanField(default=False)
@@ -2658,14 +2621,20 @@ class PollConfigStvScottish(Model):
poll_id = fields.RelationField(
to={"poll": "config_id"}, is_view_field=True, required=True
)
- option_ids = fields.RelationListField(
- to={"poll_config_option": "poll_config_id"},
- on_delete=fields.OnDelete.CASCADE,
- is_view_field=True,
- )
posts = fields.IntegerField(default=1, constraints={"minimum": 1})
+class PollOption(Model):
+ collection = "poll_option"
+ verbose_name = "poll option"
+
+ id = fields.IntegerField(required=True, constant=True)
+ poll_id = fields.RelationField(to={"poll": "option_ids"}, required=True)
+ weight = fields.IntegerField()
+ text = fields.CharField()
+ meeting_user_id = fields.RelationField(to={"meeting_user": "poll_option_ids"})
+
+
class Projection(Model):
collection = "projection"
verbose_name = "projection"
From c5449be1c6adb63c82f99900c6621746859be4bc Mon Sep 17 00:00:00 2001
From: Bastian Rihm
Date: Thu, 23 Apr 2026 12:16:23 +0200
Subject: [PATCH 05/23] Update poll options format (#3407)
---
data/example-data.json | 30 ++++-----
meta | 2 +-
.../action/actions/user/merge_mixins.py | 8 +--
openslides_backend/models/models.py | 67 +++++--------------
4 files changed, 38 insertions(+), 69 deletions(-)
diff --git a/data/example-data.json b/data/example-data.json
index f8130ed2be..6286e36385 100644
--- a/data/example-data.json
+++ b/data/example-data.json
@@ -1905,6 +1905,7 @@
"title": "2",
"config_id": "poll_config_approval/2",
"visibility": "manually",
+ "option_ids": [1, 2],
"state": "finished",
"published": true,
"allow_invalid": false,
@@ -1917,6 +1918,7 @@
"title": "3",
"config_id": "poll_config_selection/1",
"visibility": "open",
+ "option_ids": [3, 4, 5],
"state": "started",
"published": false,
"allow_invalid": false,
@@ -1931,6 +1933,7 @@
"title": "4",
"config_id": "poll_config_rating_score/1",
"visibility": "secret",
+ "option_ids": [6, 7],
"state": "created",
"published": false,
"allow_invalid": false,
@@ -1943,6 +1946,7 @@
"title": "5",
"config_id": "poll_config_rating_approval/1",
"visibility": "manually",
+ "option_ids": [8, 9, 10],
"state": "finished",
"published": false,
"allow_invalid": false,
@@ -1970,7 +1974,6 @@
"id": 2,
"poll_id": 2,
"allow_abstain": true,
- "option_ids": [1, 2],
"onehundred_percent_base": "valid"
}
},
@@ -1978,7 +1981,6 @@
"1": {
"id": 1,
"poll_id": 3,
- "option_ids": [3, 4, 5],
"max_options_amount": 2,
"min_options_amount": 1,
"allow_nota": true,
@@ -1990,7 +1992,6 @@
"1": {
"id": 1,
"poll_id": 4,
- "option_ids": [6, 7],
"max_options_amount": 2,
"min_options_amount": 2,
"max_votes_per_option": 0,
@@ -2006,61 +2007,60 @@
"max_options_amount": 0,
"min_options_amount": 0,
"allow_abstain": true,
- "option_ids": [8, 9, 10],
"onehundred_percent_base": "valid"
}
},
- "poll_config_option": {
+ "poll_option": {
"1": {
"id": 1,
"weight": 1,
- "poll_config_id": "poll_config_approval/2",
+ "poll_id": 2,
"meeting_user_id": 1
},
"2": {
"id": 2,
"weight": 2,
- "poll_config_id": "poll_config_approval/2",
+ "poll_id": 2,
"meeting_user_id": 3
},
"3": {
"id": 3,
- "poll_config_id": "poll_config_selection/1",
+ "poll_id": 3,
"text": "Option 3"
},
"4": {
"id": 4,
- "poll_config_id": "poll_config_selection/1",
+ "poll_id": 3,
"text": "Option 4"
},
"5": {
"id": 5,
- "poll_config_id": "poll_config_selection/1",
+ "poll_id": 3,
"text": "Option 5"
},
"6": {
"id": 6,
- "poll_config_id": "poll_config_rating_score/1",
+ "poll_id": 4,
"text": "option 6"
},
"7": {
"id": 7,
- "poll_config_id": "poll_config_rating_score/1",
+ "poll_id": 4,
"text": "option 7"
},
"8": {
"id": 8,
- "poll_config_id": "poll_config_rating_approval/1",
+ "poll_id": 5,
"meeting_user_id": 1
},
"9": {
"id": 9,
- "poll_config_id": "poll_config_rating_approval/1",
+ "poll_id": 5,
"meeting_user_id": 2
},
"10": {
"id": 10,
- "poll_config_id": "poll_config_rating_approval/1",
+ "poll_id": 5,
"meeting_user_id": 3
}
},
diff --git a/meta b/meta
index 594c08ae41..98929842c8 160000
--- a/meta
+++ b/meta
@@ -1 +1 @@
-Subproject commit 594c08ae412d572e42ca9ba534c41f139f2dd8cb
+Subproject commit 98929842c8aaaaed2f381e177717d07fe23625ac
diff --git a/openslides_backend/action/actions/user/merge_mixins.py b/openslides_backend/action/actions/user/merge_mixins.py
index 23c216164d..b73dad7f0f 100644
--- a/openslides_backend/action/actions/user/merge_mixins.py
+++ b/openslides_backend/action/actions/user/merge_mixins.py
@@ -12,7 +12,7 @@
MotionWorkingGroupSpeaker,
PersonalNote,
Poll,
- PollConfigOption,
+ PollOption,
Speaker,
)
from ....shared.exceptions import ActionException
@@ -207,11 +207,11 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
)
-class PollConfigOptionMergeMixin(BaseMergeMixin):
+class PollOptionMergeMixin(BaseMergeMixin):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.add_collection_field_groups(
- PollConfigOption,
+ PollOption,
{},
"meeting_user_id",
)
@@ -235,7 +235,7 @@ class MeetingUserMergeMixin(
MotionSupporterMergeMixin,
AssignmentCandidateMergeMixin,
PollMergeMixin,
- PollConfigOptionMergeMixin,
+ PollOptionMergeMixin,
BallotMergeMixin,
SpeakerMergeMixin,
):
diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py
index 9d3e1b6c51..01b818a7fd 100644
--- a/openslides_backend/models/models.py
+++ b/openslides_backend/models/models.py
@@ -1021,7 +1021,7 @@ class Meeting(Model, MeetingModelMixin):
poll_default_type = fields.CharField(default="analog")
poll_default_method = fields.CharField()
poll_default_onehundred_percent_base = fields.CharField(
- default="YNA",
+ default="valid",
constraints={
"enum": [
"no_general",
@@ -1599,9 +1599,7 @@ class MeetingUser(Model):
),
)
poll_option_ids = fields.RelationListField(
- to={"poll_config_option": "meeting_user_id"},
- is_view_field=True,
- is_primary=True,
+ to={"poll_option": "meeting_user_id"}, is_view_field=True, is_primary=True
)
acting_ballot_ids = fields.RelationListField(
to={"ballot": "acting_meeting_user_id"}, is_view_field=True
@@ -2432,6 +2430,11 @@ class Poll(Model, PollModelMixin):
},
required=True,
)
+ option_ids = fields.RelationListField(
+ to={"poll_option": "poll_id"},
+ on_delete=fields.OnDelete.CASCADE,
+ is_view_field=True,
+ )
visibility = fields.CharField(
required=True, constraints={"enum": ["manually", "named", "open", "secret"]}
)
@@ -2513,11 +2516,6 @@ class PollConfigApproval(Model):
poll_id = fields.RelationField(
to={"poll": "config_id"}, is_view_field=True, required=True
)
- option_ids = fields.RelationListField(
- to={"poll_config_option": "poll_config_id"},
- on_delete=fields.OnDelete.CASCADE,
- is_view_field=True,
- )
allow_abstain = fields.BooleanField(default=True)
onehundred_percent_base = fields.CharField(
required=True,
@@ -2534,26 +2532,6 @@ class PollConfigApproval(Model):
)
-class PollConfigOption(Model):
- collection = "poll_config_option"
- verbose_name = "poll config option"
-
- id = fields.IntegerField(required=True, constant=True)
- poll_config_id = fields.GenericRelationField(
- to={
- "poll_config_stv_scottish": "option_ids",
- "poll_config_rating_approval": "option_ids",
- "poll_config_rating_score": "option_ids",
- "poll_config_selection": "option_ids",
- "poll_config_approval": "option_ids",
- },
- required=True,
- )
- weight = fields.IntegerField()
- text = fields.CharField()
- meeting_user_id = fields.RelationField(to={"meeting_user": "poll_option_ids"})
-
-
class PollConfigRatingApproval(Model):
collection = "poll_config_rating_approval"
verbose_name = "poll config rating approval"
@@ -2562,11 +2540,6 @@ class PollConfigRatingApproval(Model):
poll_id = fields.RelationField(
to={"poll": "config_id"}, is_view_field=True, required=True
)
- option_ids = fields.RelationListField(
- to={"poll_config_option": "poll_config_id"},
- on_delete=fields.OnDelete.CASCADE,
- is_view_field=True,
- )
max_options_amount = fields.IntegerField(default=0)
min_options_amount = fields.IntegerField(default=0)
allow_abstain = fields.BooleanField(default=True)
@@ -2593,11 +2566,6 @@ class PollConfigRatingScore(Model):
poll_id = fields.RelationField(
to={"poll": "config_id"}, is_view_field=True, required=True
)
- option_ids = fields.RelationListField(
- to={"poll_config_option": "poll_config_id"},
- on_delete=fields.OnDelete.CASCADE,
- is_view_field=True,
- )
max_options_amount = fields.IntegerField(default=0)
min_options_amount = fields.IntegerField(default=0)
max_votes_per_option = fields.IntegerField(default=0)
@@ -2626,11 +2594,6 @@ class PollConfigSelection(Model):
poll_id = fields.RelationField(
to={"poll": "config_id"}, is_view_field=True, required=True
)
- option_ids = fields.RelationListField(
- to={"poll_config_option": "poll_config_id"},
- on_delete=fields.OnDelete.CASCADE,
- is_view_field=True,
- )
max_options_amount = fields.IntegerField(default=0)
min_options_amount = fields.IntegerField(default=0)
allow_nota = fields.BooleanField(default=False)
@@ -2658,14 +2621,20 @@ class PollConfigStvScottish(Model):
poll_id = fields.RelationField(
to={"poll": "config_id"}, is_view_field=True, required=True
)
- option_ids = fields.RelationListField(
- to={"poll_config_option": "poll_config_id"},
- on_delete=fields.OnDelete.CASCADE,
- is_view_field=True,
- )
posts = fields.IntegerField(default=1, constraints={"minimum": 1})
+class PollOption(Model):
+ collection = "poll_option"
+ verbose_name = "poll option"
+
+ id = fields.IntegerField(required=True, constant=True)
+ poll_id = fields.RelationField(to={"poll": "option_ids"}, required=True)
+ weight = fields.IntegerField()
+ text = fields.CharField()
+ meeting_user_id = fields.RelationField(to={"meeting_user": "poll_option_ids"})
+
+
class Projection(Model):
collection = "projection"
verbose_name = "projection"
From bf0a757dfa08bed6084e6d60d9a76efefa71d3b1 Mon Sep 17 00:00:00 2001
From: Bastian Rihm
Date: Wed, 6 May 2026 13:42:18 +0200
Subject: [PATCH 06/23] Update meta
---
data/example-data.json | 38 +++++++----------------------
meta | 2 +-
openslides_backend/models/models.py | 9 -------
3 files changed, 10 insertions(+), 39 deletions(-)
diff --git a/data/example-data.json b/data/example-data.json
index a17aa3c2fc..391fa72f33 100644
--- a/data/example-data.json
+++ b/data/example-data.json
@@ -387,17 +387,6 @@
2,
3
],
- "ballot_ids": [
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- 7,
- 8,
- 9
- ],
"poll_ballot_paper_selection": "CUSTOM_NUMBER",
"poll_ballot_paper_number": 8,
"poll_sort_poll_result_by_votes": true,
@@ -2083,64 +2072,56 @@
"weight": "2.000000",
"value": "yes",
"split": false,
- "poll_id": 1,
- "meeting_id": 1
+ "poll_id": 1
},
"2": {
"id": 2,
"weight": "4.000000",
"value": "no",
"split": false,
- "poll_id": 1,
- "meeting_id": 1
+ "poll_id": 1
},
"3": {
"id": 3,
"weight": "1.000000",
"value": "abstain",
"split": false,
- "poll_id": 1,
- "meeting_id": 1
+ "poll_id": 1
},
"4": {
"id": 4,
"value": "{\"1\":\"yes\",\"2\":\"abstain\",\"3\":\"yes\"}",
"weight": "3.000000",
"split": false,
- "poll_id": 5,
- "meeting_id": 1
+ "poll_id": 5
},
"5": {
"id": 5,
"value": "{\"1\":\"no\",\"2\":\"yes\",\"3\":\"yes\"}",
"weight": "7.000000",
"split": false,
- "poll_id": 5,
- "meeting_id": 1
+ "poll_id": 5
},
"6": {
"id": 6,
"value": "{\"1\":\"yes\",\"2\":\"no\",\"3\":\"no\"}",
"weight": "2.000000",
"split": false,
- "poll_id": 5,
- "meeting_id": 1
+ "poll_id": 5
},
"7": {
"id": 7,
"value": "{\"1\":\"no\",\"2\":\"no\",\"3\":\"yes\"}",
"weight": "2.000000",
"split": false,
- "poll_id": 5,
- "meeting_id": 1
+ "poll_id": 5
},
"8": {
"id": 8,
"value": "{\"1\":\"abstain\",\"2\":\"yes\",\"3\":\"abstain\"}",
"weight": "1.000000",
"split": false,
- "poll_id": 5,
- "meeting_id": 1
+ "poll_id": 5
},
"9": {
"id": 9,
@@ -2149,8 +2130,7 @@
"acting_meeting_user_id": 1,
"represented_meeting_user_id": 1,
"split": false,
- "poll_id": 3,
- "meeting_id": 1
+ "poll_id": 3
}
},
"assignment": {
diff --git a/meta b/meta
index 6b8e508efd..30ec47f1b9 160000
--- a/meta
+++ b/meta
@@ -1 +1 @@
-Subproject commit 6b8e508efd7a2fef2baa49e1b0bfce3bebe826fc
+Subproject commit 30ec47f1b98ee78866a7b90f50202b77b757aa43
diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py
index 65ea0b6597..9c5028a487 100644
--- a/openslides_backend/models/models.py
+++ b/openslides_backend/models/models.py
@@ -186,9 +186,6 @@ class Ballot(Model):
weight = fields.DecimalField(constant=True, default="1.000000")
split = fields.BooleanField(default=False)
value = fields.TextField(constant=True)
- meeting_id = fields.RelationField(
- to={"meeting": "ballot_ids"}, required=True, constant=True
- )
poll_id = fields.RelationField(
to={"poll": "ballot_ids"}, required=True, constant=True
)
@@ -1169,12 +1166,6 @@ class Meeting(Model, MeetingModelMixin):
is_view_field=True,
is_primary=True,
)
- ballot_ids = fields.RelationListField(
- to={"ballot": "meeting_id"},
- on_delete=fields.OnDelete.CASCADE,
- is_view_field=True,
- is_primary=True,
- )
assignment_ids = fields.RelationListField(
to={"assignment": "meeting_id"},
on_delete=fields.OnDelete.CASCADE,
From d46c3233d98f746f6682f2c393f981338782aa4c Mon Sep 17 00:00:00 2001
From: vkrasnovyd
Date: Thu, 7 May 2026 14:45:08 +0200
Subject: [PATCH 07/23] Update meta hash
---
meta | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/meta b/meta
index f2c8102f95..4a2ea7e283 160000
--- a/meta
+++ b/meta
@@ -1 +1 @@
-Subproject commit f2c8102f95d20b54abf033d38d291b97af3f9363
+Subproject commit 4a2ea7e283ab78db257bd0848d69744c44f119e5
From 57574f1a76d07d1b0a0e83be72fcb1945ef6423b Mon Sep 17 00:00:00 2001
From: vkrasnovyd
Date: Thu, 7 May 2026 14:45:26 +0200
Subject: [PATCH 08/23] Lint
---
tests/system/action/meeting/test_import.py | 1 -
tests/system/presenter/test_check_database.py | 1 -
tests/system/presenter/test_check_database_all.py | 1 -
3 files changed, 3 deletions(-)
diff --git a/tests/system/action/meeting/test_import.py b/tests/system/action/meeting/test_import.py
index eff5d682e8..faa7543060 100644
--- a/tests/system/action/meeting/test_import.py
+++ b/tests/system/action/meeting/test_import.py
@@ -1,7 +1,6 @@
import base64
import time
from copy import deepcopy
-from decimal import Decimal
from typing import Any
from psycopg.types.json import Jsonb
diff --git a/tests/system/presenter/test_check_database.py b/tests/system/presenter/test_check_database.py
index 15576a9bd6..ab863f3e80 100644
--- a/tests/system/presenter/test_check_database.py
+++ b/tests/system/presenter/test_check_database.py
@@ -1,4 +1,3 @@
-from decimal import Decimal
from typing import Any
from psycopg.types.json import Jsonb
diff --git a/tests/system/presenter/test_check_database_all.py b/tests/system/presenter/test_check_database_all.py
index 1152beb8c4..e3e3be4e09 100644
--- a/tests/system/presenter/test_check_database_all.py
+++ b/tests/system/presenter/test_check_database_all.py
@@ -1,5 +1,4 @@
from datetime import datetime, timedelta
-from decimal import Decimal
from typing import Any
from openslides_backend.action.action_worker import ActionWorkerState
From 8cf8900a157fe2e15f79a0c169278a9ae59ac977 Mon Sep 17 00:00:00 2001
From: Viktoriia Krasnovyd <114735598+vkrasnovyd@users.noreply.github.com>
Date: Mon, 11 May 2026 13:46:34 +0200
Subject: [PATCH 09/23] Update models and permissions, use managed_by (#3559)
---
cli/generate_models.py | 14 ++-
data/example-data.json | 7 +-
meta | 2 +-
.../action/actions/user/merge_mixins.py | 8 +-
.../action/actions/user/merge_together.py | 4 +-
openslides_backend/models/models.py | 112 ++++++++++++++----
openslides_backend/permissions/permissions.py | 20 ++--
openslides_backend/shared/export_helper.py | 14 ++-
tests/system/action/meeting/test_import.py | 4 +-
.../system/action/test_equal_fields_check.py | 2 +-
tests/system/presenter/test_export_meeting.py | 4 +-
11 files changed, 140 insertions(+), 51 deletions(-)
diff --git a/cli/generate_models.py b/cli/generate_models.py
index cd646789ef..018b09d8ca 100644
--- a/cli/generate_models.py
+++ b/cli/generate_models.py
@@ -106,7 +106,7 @@ def main() -> None:
for collection, data in InternalHelper.MODELS.items():
if collection.startswith("_"):
continue
- model = Model(collection, data["fields"])
+ model = Model(collection, data.get("managed_by"), data["fields"])
dest.write(model.get_code())
if args.check:
@@ -128,17 +128,26 @@ class Node:
class Model(Node):
collection: str
+ managed_by: str
attributes: dict[str, "Attribute"]
MODEL_TEMPLATE = string.Template(dedent("""
class ${class_name}(${base_classes}):
collection = "${collection}"
verbose_name = "${verbose_name}"
+ managed_by = "${managed_by}"
"""))
- def __init__(self, collection: str, fields: dict[str, dict[str, Any]]) -> None:
+ def __init__(
+ self, collection: str, managed_by: str | None, fields: dict[str, dict[str, Any]]
+ ) -> None:
self.collection = collection
assert collection
+ self.managed_by = managed_by or "backend"
+ assert self.managed_by in [
+ "backend",
+ "vote",
+ ], f"Forbidden value for {collection}/managed_by: '{managed_by}'. Available options are: 'backend', 'vote'."
self.attributes = {}
for field_name, field in fields.items():
if field_name == "_meta":
@@ -161,6 +170,7 @@ def get_code(self) -> str:
"base_classes": ", ".join(cls.__name__ for cls in base_classes),
"collection": self.collection,
"verbose_name": verbose_name,
+ "managed_by": self.managed_by,
}
)
+ "\n"
diff --git a/data/example-data.json b/data/example-data.json
index 391fa72f33..3d505050af 100644
--- a/data/example-data.json
+++ b/data/example-data.json
@@ -676,7 +676,10 @@
"meeting.can_see_history",
"motion.can_manage",
"motion.can_manage_polls",
- "poll.can_manage",
+ "agenda_item.can_manage_polls",
+ "assignment.can_manage_polls",
+ "motion.can_manage_polls",
+ "poll.can_see_progress",
"projector.can_manage",
"tag.can_manage",
"user.can_manage"
@@ -2066,7 +2069,7 @@
"meeting_user_id": 3
}
},
- "ballot": {
+ "poll_ballot": {
"1": {
"id": 1,
"weight": "2.000000",
diff --git a/meta b/meta
index 4a2ea7e283..0ffd96331f 160000
--- a/meta
+++ b/meta
@@ -1 +1 @@
-Subproject commit 4a2ea7e283ab78db257bd0848d69744c44f119e5
+Subproject commit 0ffd96331f81d6e0a671fa591bcf3de6bfa71a91
diff --git a/openslides_backend/action/actions/user/merge_mixins.py b/openslides_backend/action/actions/user/merge_mixins.py
index b73dad7f0f..13ea1622cc 100644
--- a/openslides_backend/action/actions/user/merge_mixins.py
+++ b/openslides_backend/action/actions/user/merge_mixins.py
@@ -4,7 +4,6 @@
from ....models.models import (
AssignmentCandidate,
- Ballot,
MeetingUser,
MotionEditor,
MotionSubmitter,
@@ -12,6 +11,7 @@
MotionWorkingGroupSpeaker,
PersonalNote,
Poll,
+ PollBallot,
PollOption,
Speaker,
)
@@ -217,11 +217,11 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
)
-class BallotMergeMixin(BaseMergeMixin):
+class PollBallotMergeMixin(BaseMergeMixin):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.add_collection_field_groups(
- Ballot,
+ PollBallot,
{},
"meeting_user_id",
)
@@ -235,8 +235,8 @@ class MeetingUserMergeMixin(
MotionSupporterMergeMixin,
AssignmentCandidateMergeMixin,
PollMergeMixin,
+ PollBallotMergeMixin,
PollOptionMergeMixin,
- BallotMergeMixin,
SpeakerMergeMixin,
):
def __init__(self, *args: Any, **kwargs: Any) -> None:
diff --git a/openslides_backend/action/actions/user/merge_together.py b/openslides_backend/action/actions/user/merge_together.py
index 6f76a94f85..74daecc84a 100644
--- a/openslides_backend/action/actions/user/merge_together.py
+++ b/openslides_backend/action/actions/user/merge_together.py
@@ -438,7 +438,7 @@ def check_polls(self, into: PartialModel, other_models: list[PartialModel]) -> N
["poll_config_id"],
),
GetManyRequest(
- "ballot",
+ "poll_ballot",
v_ids,
["poll_id"],
),
@@ -496,7 +496,7 @@ def check_polls(self, into: PartialModel, other_models: list[PartialModel]) -> N
},
}
- ballot_data = many_models["ballot"]
+ ballot_data = many_models["poll_ballot"]
ballot_poll_ids_per_user_id[model["user_id"]] = {
*ballot_poll_ids_per_user_id.get(model["user_id"], set()),
*{
diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py
index d4365cf0a7..f80e9c9c3e 100644
--- a/openslides_backend/models/models.py
+++ b/openslides_backend/models/models.py
@@ -8,6 +8,7 @@
class ActionWorker(Model):
collection = "action_worker"
verbose_name = "action worker"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -29,6 +30,7 @@ class ActionWorker(Model):
class AgendaItem(Model, AgendaItemModelMixin):
collection = "agenda_item"
verbose_name = "agenda item"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
item_number = fields.CharField()
@@ -87,6 +89,7 @@ class AgendaItem(Model, AgendaItemModelMixin):
class Assignment(Model):
collection = "assignment"
verbose_name = "assignment"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
title = fields.CharField(required=True)
@@ -164,6 +167,7 @@ class Assignment(Model):
class AssignmentCandidate(Model):
collection = "assignment_candidate"
verbose_name = "assignment candidate"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
weight = fields.IntegerField(default=10000)
@@ -178,28 +182,10 @@ class AssignmentCandidate(Model):
)
-class Ballot(Model):
- collection = "ballot"
- verbose_name = "ballot"
-
- id = fields.IntegerField(required=True, constant=True)
- weight = fields.DecimalField(constant=True, default="1.000000")
- split = fields.BooleanField(default=False)
- value = fields.TextField(constant=True)
- poll_id = fields.RelationField(
- to={"poll": "ballot_ids"}, required=True, constant=True
- )
- acting_meeting_user_id = fields.RelationField(
- to={"meeting_user": "acting_ballot_ids"}
- )
- represented_meeting_user_id = fields.RelationField(
- to={"meeting_user": "represented_ballot_ids"}
- )
-
-
class ChatGroup(Model):
collection = "chat_group"
verbose_name = "chat group"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -239,6 +225,7 @@ class ChatGroup(Model):
class ChatMessage(Model):
collection = "chat_message"
verbose_name = "chat message"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
content = fields.HTMLStrictField(required=True)
@@ -257,6 +244,7 @@ class ChatMessage(Model):
class Committee(Model):
collection = "committee"
verbose_name = "committee"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -351,6 +339,7 @@ class Committee(Model):
class Gender(Model):
collection = "gender"
verbose_name = "gender"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True, unique=True)
@@ -365,6 +354,7 @@ class Gender(Model):
class Group(Model):
collection = "group"
verbose_name = "group"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
external_id = fields.CharField()
@@ -373,12 +363,15 @@ class Group(Model):
in_array_constraints={
"enum": [
"agenda_item.can_manage",
+ "agenda_item.can_manage_polls",
+ "agenda_item.can_see_polls",
"agenda_item.can_see",
"agenda_item.can_see_internal",
"assignment.can_manage",
"assignment.can_manage_polls",
"assignment.can_nominate_other",
"assignment.can_nominate_self",
+ "assignment.can_see_polls",
"assignment.can_see",
"chat.can_manage",
"list_of_speakers.can_be_speaker",
@@ -400,11 +393,11 @@ class Group(Model):
"motion.can_manage",
"motion.can_manage_metadata",
"motion.can_manage_polls",
+ "motion.can_see_polls",
"motion.can_see",
"motion.can_see_internal",
"motion.can_see_origin",
"motion.can_support",
- "poll.can_manage",
"poll.can_see_progress",
"projector.can_manage",
"projector.can_see",
@@ -538,6 +531,7 @@ class Group(Model):
class HistoryEntry(Model):
collection = "history_entry"
verbose_name = "history entry"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
entries = fields.TextArrayField()
@@ -558,6 +552,7 @@ class HistoryEntry(Model):
class HistoryPosition(Model):
collection = "history_position"
verbose_name = "history position"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
timestamp = fields.TimestampField(read_only=True)
@@ -573,6 +568,7 @@ class HistoryPosition(Model):
class ImportPreview(Model):
collection = "import_preview"
verbose_name = "import preview"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(
@@ -591,6 +587,7 @@ class ImportPreview(Model):
class ListOfSpeakers(Model):
collection = "list_of_speakers"
verbose_name = "list of speakers"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
closed = fields.BooleanField(default=False)
@@ -638,6 +635,7 @@ class ListOfSpeakers(Model):
class Mediafile(Model):
collection = "mediafile"
verbose_name = "mediafile"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
title = fields.CharField(
@@ -680,6 +678,7 @@ class Mediafile(Model):
class Meeting(Model, MeetingModelMixin):
collection = "meeting"
verbose_name = "meeting"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
external_id = fields.CharField(unique=True)
@@ -1382,6 +1381,7 @@ class Meeting(Model, MeetingModelMixin):
class MeetingMediafile(Model):
collection = "meeting_mediafile"
verbose_name = "meeting mediafile"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
mediafile_id = fields.RelationField(
@@ -1501,6 +1501,7 @@ class MeetingMediafile(Model):
class MeetingUser(Model):
collection = "meeting_user"
verbose_name = "meeting user"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
comment = fields.HTMLStrictField()
@@ -1558,10 +1559,10 @@ class MeetingUser(Model):
to={"poll_option": "meeting_user_id"}, is_view_field=True, is_primary=True
)
acting_ballot_ids = fields.RelationListField(
- to={"ballot": "acting_meeting_user_id"}, is_view_field=True
+ to={"poll_ballot": "acting_meeting_user_id"}, is_view_field=True
)
represented_ballot_ids = fields.RelationListField(
- to={"ballot": "represented_meeting_user_id"}, is_view_field=True
+ to={"poll_ballot": "represented_meeting_user_id"}, is_view_field=True
)
chat_message_ids = fields.RelationListField(
to={"chat_message": "meeting_user_id"}, is_view_field=True
@@ -1593,6 +1594,7 @@ class MeetingUser(Model):
class Motion(Model):
collection = "motion"
verbose_name = "motion"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
number = fields.CharField()
@@ -1811,6 +1813,7 @@ class Motion(Model):
class MotionBlock(Model):
collection = "motion_block"
verbose_name = "motion block"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
title = fields.CharField(required=True)
@@ -1849,6 +1852,7 @@ class MotionBlock(Model):
class MotionCategory(Model):
collection = "motion_category"
verbose_name = "motion category"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -1881,6 +1885,7 @@ class MotionCategory(Model):
class MotionChangeRecommendation(Model):
collection = "motion_change_recommendation"
verbose_name = "motion change recommendation"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
rejected = fields.BooleanField(default=False)
@@ -1905,6 +1910,7 @@ class MotionChangeRecommendation(Model):
class MotionComment(Model):
collection = "motion_comment"
verbose_name = "motion comment"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
comment = fields.HTMLStrictField(required=True)
@@ -1922,6 +1928,7 @@ class MotionComment(Model):
class MotionCommentSection(Model):
collection = "motion_comment_section"
verbose_name = "motion comment section"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -1969,6 +1976,7 @@ class MotionCommentSection(Model):
class MotionEditor(Model):
collection = "motion_editor"
verbose_name = "motion editor"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
weight = fields.IntegerField()
@@ -1984,6 +1992,7 @@ class MotionEditor(Model):
class MotionState(Model):
collection = "motion_state"
verbose_name = "motion state"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -2070,6 +2079,7 @@ class MotionState(Model):
class MotionSubmitter(Model):
collection = "motion_submitter"
verbose_name = "motion submitter"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
weight = fields.IntegerField()
@@ -2085,6 +2095,7 @@ class MotionSubmitter(Model):
class MotionSupporter(Model):
collection = "motion_supporter"
verbose_name = "motion supporter"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
meeting_user_id = fields.RelationField(to={"meeting_user": "motion_supporter_ids"})
@@ -2099,6 +2110,7 @@ class MotionSupporter(Model):
class MotionWorkflow(Model):
collection = "motion_workflow"
verbose_name = "motion workflow"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -2133,6 +2145,7 @@ class MotionWorkflow(Model):
class MotionWorkingGroupSpeaker(Model):
collection = "motion_working_group_speaker"
verbose_name = "motion working group speaker"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
weight = fields.IntegerField()
@@ -2150,6 +2163,7 @@ class MotionWorkingGroupSpeaker(Model):
class Organization(Model):
collection = "organization"
verbose_name = "organization"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField()
@@ -2236,6 +2250,7 @@ class Organization(Model):
class OrganizationTag(Model):
collection = "organization_tag"
verbose_name = "organization tag"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -2262,6 +2277,7 @@ class OrganizationTag(Model):
class PersonalNote(Model):
collection = "personal_note"
verbose_name = "personal note"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
note = fields.HTMLStrictField()
@@ -2280,6 +2296,7 @@ class PersonalNote(Model):
class PointOfOrderCategory(Model):
collection = "point_of_order_category"
verbose_name = "point of order category"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
text = fields.CharField(required=True)
@@ -2295,6 +2312,7 @@ class PointOfOrderCategory(Model):
class Poll(Model, PollModelMixin):
collection = "poll"
verbose_name = "poll"
+ managed_by = "vote"
id = fields.IntegerField(required=True, constant=True)
title = fields.CharField(required=True)
@@ -2327,6 +2345,12 @@ class Poll(Model, PollModelMixin):
published = fields.BooleanField(
default=False, constraints={"description": "If true, users can see the result."}
)
+ anonymized = fields.BooleanField(
+ default=False,
+ constraints={
+ "description": "Set to true, after finished was called with anonymize."
+ },
+ )
allow_invalid = fields.BooleanField(
default=False,
constraints={
@@ -2337,6 +2361,10 @@ class Poll(Model, PollModelMixin):
default=False,
constraints={"description": "If true, users can split there vote."},
)
+ live_voting_enabled = fields.BooleanField(
+ default=False,
+ constraints={"description": "If true, on start, publish will be set to true."},
+ )
sequential_number = fields.IntegerField(
required=True,
read_only=True,
@@ -2352,7 +2380,7 @@ class Poll(Model, PollModelMixin):
constant=True,
)
ballot_ids = fields.RelationListField(
- to={"ballot": "poll_id"},
+ to={"poll_ballot": "poll_id"},
on_delete=fields.OnDelete.CASCADE,
is_view_field=True,
is_primary=True,
@@ -2382,9 +2410,30 @@ class Poll(Model, PollModelMixin):
)
+class PollBallot(Model):
+ collection = "poll_ballot"
+ verbose_name = "poll ballot"
+ managed_by = "vote"
+
+ id = fields.IntegerField(required=True, constant=True)
+ weight = fields.DecimalField(constant=True, default="1.000000")
+ split = fields.BooleanField(default=False)
+ value = fields.TextField(constant=True)
+ poll_id = fields.RelationField(
+ to={"poll": "ballot_ids"}, required=True, constant=True
+ )
+ acting_meeting_user_id = fields.RelationField(
+ to={"meeting_user": "acting_ballot_ids"}
+ )
+ represented_meeting_user_id = fields.RelationField(
+ to={"meeting_user": "represented_ballot_ids"}
+ )
+
+
class PollConfigApproval(Model):
collection = "poll_config_approval"
verbose_name = "poll config approval"
+ managed_by = "vote"
id = fields.IntegerField(required=True, constant=True)
poll_id = fields.RelationField(
@@ -2409,6 +2458,7 @@ class PollConfigApproval(Model):
class PollConfigRatingApproval(Model):
collection = "poll_config_rating_approval"
verbose_name = "poll config rating approval"
+ managed_by = "vote"
id = fields.IntegerField(required=True, constant=True)
poll_id = fields.RelationField(
@@ -2435,6 +2485,7 @@ class PollConfigRatingApproval(Model):
class PollConfigRatingScore(Model):
collection = "poll_config_rating_score"
verbose_name = "poll config rating score"
+ managed_by = "vote"
id = fields.IntegerField(required=True, constant=True)
poll_id = fields.RelationField(
@@ -2463,6 +2514,7 @@ class PollConfigRatingScore(Model):
class PollConfigSelection(Model):
collection = "poll_config_selection"
verbose_name = "poll config selection"
+ managed_by = "vote"
id = fields.IntegerField(required=True, constant=True)
poll_id = fields.RelationField(
@@ -2485,11 +2537,13 @@ class PollConfigSelection(Model):
]
},
)
+ display_chart = fields.CharField()
class PollConfigStvScottish(Model):
collection = "poll_config_stv_scottish"
verbose_name = "poll config stv scottish"
+ managed_by = "vote"
id = fields.IntegerField(required=True, constant=True)
poll_id = fields.RelationField(
@@ -2501,6 +2555,7 @@ class PollConfigStvScottish(Model):
class PollOption(Model):
collection = "poll_option"
verbose_name = "poll option"
+ managed_by = "vote"
id = fields.IntegerField(required=True, constant=True)
poll_id = fields.RelationField(to={"poll": "option_ids"}, required=True)
@@ -2512,6 +2567,7 @@ class PollOption(Model):
class Projection(Model):
collection = "projection"
verbose_name = "projection"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
options = fields.JSONField()
@@ -2552,6 +2608,7 @@ class Projection(Model):
class Projector(Model):
collection = "projector"
verbose_name = "projector"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -2653,6 +2710,7 @@ class Projector(Model):
class ProjectorCountdown(Model):
collection = "projector_countdown"
verbose_name = "projector countdown"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
title = fields.CharField(required=True)
@@ -2679,6 +2737,7 @@ class ProjectorCountdown(Model):
class ProjectorMessage(Model):
collection = "projector_message"
verbose_name = "projector message"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
message = fields.HTMLStrictField(required=True)
@@ -2695,6 +2754,7 @@ class ProjectorMessage(Model):
class Speaker(Model):
collection = "speaker"
verbose_name = "speaker"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
begin_time = fields.TimestampField()
@@ -2735,6 +2795,7 @@ class Speaker(Model):
class StructureLevel(Model):
collection = "structure_level"
verbose_name = "structure level"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -2762,6 +2823,7 @@ class StructureLevel(Model):
class StructureLevelListOfSpeakers(Model):
collection = "structure_level_list_of_speakers"
verbose_name = "structure level list of speakers"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
structure_level_id = fields.RelationField(
@@ -2806,6 +2868,7 @@ class StructureLevelListOfSpeakers(Model):
class Tag(Model):
collection = "tag"
verbose_name = "tag"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -2832,6 +2895,7 @@ class Tag(Model):
class Theme(Model):
collection = "theme"
verbose_name = "theme"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -2892,6 +2956,7 @@ class Theme(Model):
class Topic(Model):
collection = "topic"
verbose_name = "topic"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
title = fields.CharField(required=True)
@@ -2947,6 +3012,7 @@ class Topic(Model):
class User(Model):
collection = "user"
verbose_name = "user"
+ managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
username = fields.CharField(required=True, unique=True)
diff --git a/openslides_backend/permissions/permissions.py b/openslides_backend/permissions/permissions.py
index 82e34bef4c..d394f4f941 100644
--- a/openslides_backend/permissions/permissions.py
+++ b/openslides_backend/permissions/permissions.py
@@ -7,8 +7,10 @@
class _AgendaItem(Permission, StrEnum):
CAN_MANAGE = "agenda_item.can_manage"
+ CAN_MANAGE_POLLS = "agenda_item.can_manage_polls"
CAN_SEE = "agenda_item.can_see"
CAN_SEE_INTERNAL = "agenda_item.can_see_internal"
+ CAN_SEE_POLLS = "agenda_item.can_see_polls"
class _Assignment(Permission, StrEnum):
@@ -17,6 +19,7 @@ class _Assignment(Permission, StrEnum):
CAN_NOMINATE_OTHER = "assignment.can_nominate_other"
CAN_NOMINATE_SELF = "assignment.can_nominate_self"
CAN_SEE = "assignment.can_see"
+ CAN_SEE_POLLS = "assignment.can_see_polls"
class _Chat(Permission, StrEnum):
@@ -55,11 +58,11 @@ class _Motion(Permission, StrEnum):
CAN_SEE = "motion.can_see"
CAN_SEE_INTERNAL = "motion.can_see_internal"
CAN_SEE_ORIGIN = "motion.can_see_origin"
+ CAN_SEE_POLLS = "motion.can_see_polls"
CAN_SUPPORT = "motion.can_support"
class _Poll(Permission, StrEnum):
- CAN_MANAGE = "poll.can_manage"
CAN_SEE_PROGRESS = "poll.can_see_progress"
@@ -97,16 +100,19 @@ class Permissions:
# Holds the corresponding parent for each permission.
permission_parents: dict[Permission, list[Permission]] = {
- _AgendaItem.CAN_SEE: [_AgendaItem.CAN_SEE_INTERNAL],
- _AgendaItem.CAN_SEE_INTERNAL: [_AgendaItem.CAN_MANAGE],
+ _AgendaItem.CAN_SEE: [_AgendaItem.CAN_SEE_INTERNAL, _AgendaItem.CAN_SEE_INTERNAL],
+ _AgendaItem.CAN_SEE_INTERNAL: [_AgendaItem.CAN_MANAGE, _AgendaItem.CAN_SEE_POLLS],
_AgendaItem.CAN_MANAGE: [],
+ _AgendaItem.CAN_SEE_POLLS: [_AgendaItem.CAN_MANAGE_POLLS],
+ _AgendaItem.CAN_MANAGE_POLLS: [],
_Assignment.CAN_SEE: [
_Assignment.CAN_NOMINATE_OTHER,
- _Assignment.CAN_MANAGE_POLLS,
+ _Assignment.CAN_SEE_POLLS,
_Assignment.CAN_NOMINATE_SELF,
],
_Assignment.CAN_NOMINATE_OTHER: [_Assignment.CAN_MANAGE],
_Assignment.CAN_MANAGE: [],
+ _Assignment.CAN_SEE_POLLS: [_Assignment.CAN_MANAGE_POLLS],
_Assignment.CAN_MANAGE_POLLS: [],
_Assignment.CAN_NOMINATE_SELF: [],
_Chat.CAN_MANAGE: [],
@@ -131,7 +137,7 @@ class Permissions:
_Motion.CAN_CREATE,
_Motion.CAN_CREATE_AMENDMENTS,
_Motion.CAN_FORWARD,
- _Motion.CAN_MANAGE_POLLS,
+ _Motion.CAN_SEE_POLLS,
_Motion.CAN_SUPPORT,
_Motion.CAN_SEE_ORIGIN,
],
@@ -141,11 +147,11 @@ class Permissions:
_Motion.CAN_CREATE_AMENDMENTS: [_Motion.CAN_MANAGE],
_Motion.CAN_FORWARD: [_Motion.CAN_MANAGE],
_Motion.CAN_MANAGE: [],
+ _Motion.CAN_SEE_POLLS: [_Motion.CAN_MANAGE_POLLS],
_Motion.CAN_MANAGE_POLLS: [],
_Motion.CAN_SUPPORT: [],
_Motion.CAN_SEE_ORIGIN: [],
- _Poll.CAN_SEE_PROGRESS: [_Poll.CAN_MANAGE],
- _Poll.CAN_MANAGE: [],
+ _Poll.CAN_SEE_PROGRESS: [],
_Projector.CAN_SEE: [_Projector.CAN_MANAGE],
_Projector.CAN_MANAGE: [],
_Tag.CAN_MANAGE: [],
diff --git a/openslides_backend/shared/export_helper.py b/openslides_backend/shared/export_helper.py
index 710cff48e4..ada4b93633 100644
--- a/openslides_backend/shared/export_helper.py
+++ b/openslides_backend/shared/export_helper.py
@@ -15,7 +15,7 @@
RelationField,
RelationListField,
)
-from ..models.models import Ballot, Meeting
+from ..models.models import Meeting, PollBallot
from ..services.database.commands import GetManyRequest
from ..services.database.interface import Database
from .patterns import collection_from_fqid, fqid_from_collection_and_id, id_from_fqid
@@ -217,14 +217,18 @@ def export_meeting(
]
if ballot_ids:
ballots = datastore.get_many(
- [GetManyRequest("ballot", ballot_ids, get_fields_for_export("ballot"))],
+ [
+ GetManyRequest(
+ "poll_ballot", ballot_ids, get_fields_for_export("poll_ballot")
+ )
+ ],
lock_result=False,
use_changed_models=False,
- )["ballot"]
+ )["poll_ballot"]
for ballot_id, instance in ballots.items():
- export.setdefault("ballot", {})[str(ballot_id)] = instance
+ export.setdefault("poll_ballot", {})[str(ballot_id)] = instance
for field_name, value in instance.items():
- model_field = Ballot().get_field(field_name)
+ model_field = PollBallot().get_field(field_name)
if (
not isinstance(model_field, RelationField)
or model_field.get_own_field_name() == "poll_id"
diff --git a/tests/system/action/meeting/test_import.py b/tests/system/action/meeting/test_import.py
index faa7543060..53a7dc6004 100644
--- a/tests/system/action/meeting/test_import.py
+++ b/tests/system/action/meeting/test_import.py
@@ -2337,7 +2337,7 @@ def test_import_new_user_with_vote(self) -> None:
)
data = self.create_request_data(
{
- "ballot": {
+ "poll_ballot": {
"1": {
"id": 1,
"acting_meeting_user_id": 1,
@@ -2541,7 +2541,7 @@ def test_import_existing_user_with_vote(self) -> None:
)
data = self.create_request_data(
{
- "ballot": {
+ "poll_ballot": {
"1": {
"id": 1,
"acting_meeting_user_id": 1,
diff --git a/tests/system/action/test_equal_fields_check.py b/tests/system/action/test_equal_fields_check.py
index 5235c5d49c..6819a7943a 100644
--- a/tests/system/action/test_equal_fields_check.py
+++ b/tests/system/action/test_equal_fields_check.py
@@ -322,7 +322,7 @@ def build_model_class(
equal_field: str,
) -> None:
coll = get_collection_name(rel_type, collection, equal_field)
- ef_model = Model(coll, loaded_yml[coll]["fields"])
+ ef_model = Model(coll, "backend", loaded_yml[coll]["fields"])
field_name = generate_collection_field_name(
rel_type.split(":")[0 if coll_abbrev == "b" else 1], coll_abbrev == "c"
)[0]
diff --git a/tests/system/presenter/test_export_meeting.py b/tests/system/presenter/test_export_meeting.py
index d8f432ee8a..bfe5903e2c 100644
--- a/tests/system/presenter/test_export_meeting.py
+++ b/tests/system/presenter/test_export_meeting.py
@@ -288,8 +288,8 @@ def test_export_meeting_find_special_users(self) -> None:
assert data["poll"]["80"]["voted_ids"] == [114]
assert data["meeting_user"]["114"]["poll_voted_ids"] == [80]
- assert data["ballot"]["120"]["c"] == 114
- assert data["ballot"]["120"]["represented_meeting_user_id"] == 114
+ assert data["poll_ballot"]["120"]["c"] == 114
+ assert data["poll_ballot"]["120"]["represented_meeting_user_id"] == 114
assert data["meeting_user"]["114"]["acting_ballot_ids"] == [120]
assert data["meeting_user"]["114"]["represented_ballot_ids"] == [120]
From b09dde8def47a8e4228f3ee08b6ba8a900d1d5fb Mon Sep 17 00:00:00 2001
From: Bastian Rihm
Date: Tue, 12 May 2026 10:13:01 +0200
Subject: [PATCH 10/23] Fix example data (#3565)
---
data/example-data.json | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/data/example-data.json b/data/example-data.json
index 3d505050af..83a2ea9fe5 100644
--- a/data/example-data.json
+++ b/data/example-data.json
@@ -1894,6 +1894,8 @@
"config_id": "poll_config_approval/1",
"visibility": "manually",
"state": "finished",
+ "anonymized": true,
+ "live_voting_enabled": false,
"published": true,
"allow_invalid": false,
"allow_vote_split": false,
@@ -1912,6 +1914,8 @@
"visibility": "manually",
"option_ids": [1, 2],
"state": "finished",
+ "anonymized": true,
+ "live_voting_enabled": false,
"published": true,
"allow_invalid": false,
"allow_vote_split": false,
@@ -1925,6 +1929,8 @@
"visibility": "open",
"option_ids": [3, 4, 5],
"state": "started",
+ "anonymized": false,
+ "live_voting_enabled": false,
"published": false,
"allow_invalid": false,
"allow_vote_split": false,
@@ -1940,6 +1946,8 @@
"visibility": "secret",
"option_ids": [6, 7],
"state": "created",
+ "anonymized": false,
+ "live_voting_enabled": false,
"published": false,
"allow_invalid": false,
"allow_vote_split": false,
@@ -1953,6 +1961,8 @@
"visibility": "manually",
"option_ids": [8, 9, 10],
"state": "finished",
+ "anonymized": false,
+ "live_voting_enabled": false,
"published": false,
"allow_invalid": false,
"allow_vote_split": false,
From 3d8b61784b533d1bb5822e8b012e349daa1a4381 Mon Sep 17 00:00:00 2001
From: Viktoriia Krasnovyd <114735598+vkrasnovyd@users.noreply.github.com>
Date: Mon, 1 Jun 2026 15:20:54 +0200
Subject: [PATCH 11/23] Delete poll with vote adapter (#3590)
---
dev/docker-compose.dev.yml | 2 +-
.../action/actions/meeting/delete.py | 12 ----
openslides_backend/action/generics/delete.py | 18 ++++-
openslides_backend/models/base.py | 3 +
openslides_backend/services/vote/adapter.py | 65 ++++++++++++++-----
openslides_backend/services/vote/interface.py | 3 +
tests/system/action/meeting/test_delete.py | 11 +++-
.../system/action/user/test_merge_together.py | 8 ++-
8 files changed, 86 insertions(+), 36 deletions(-)
diff --git a/dev/docker-compose.dev.yml b/dev/docker-compose.dev.yml
index 96a5fd62a5..1b9515dfc4 100644
--- a/dev/docker-compose.dev.yml
+++ b/dev/docker-compose.dev.yml
@@ -50,7 +50,7 @@ services:
- redis
vote:
build:
- context: "https://github.com/OpenSlides/openslides-vote-service.git#${COMPOSE_REFERENCE_BRANCH:-main}"
+ context: "https://github.com/OpenSlides/openslides-vote-service.git#feature/vote" # TODO: change branch back to the main
target: "dev"
args:
CONTEXT: "dev"
diff --git a/openslides_backend/action/actions/meeting/delete.py b/openslides_backend/action/actions/meeting/delete.py
index a30f78649a..4968b73213 100644
--- a/openslides_backend/action/actions/meeting/delete.py
+++ b/openslides_backend/action/actions/meeting/delete.py
@@ -25,15 +25,3 @@ def get_committee_id(self, instance: dict[str, Any]) -> int:
["committee_id"],
)
return meeting["committee_id"]
-
- def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
- """
- Handle deletion of polls and all the related instances in the vote service.
- """
- poll_ids = self.datastore.get(f"meeting/{instance['id']}", ["poll_ids"]).get(
- "poll_ids", []
- )
- for poll_id in poll_ids:
- self.vote_service.delete(poll_id)
- self.datastore.apply_to_be_deleted(f"poll/{poll_id}")
- return instance
diff --git a/openslides_backend/action/generics/delete.py b/openslides_backend/action/generics/delete.py
index b5a39361de..bbb9b8d5d0 100644
--- a/openslides_backend/action/generics/delete.py
+++ b/openslides_backend/action/generics/delete.py
@@ -1,6 +1,7 @@
from collections.abc import Iterable
from typing import Any, cast
+from ...models.base import collections_managed_by_vote
from ...models.fields import BaseRelationField, OnDelete
from ...shared.exceptions import ActionException, ProtectedModelsException
from ...shared.interfaces.event import Event, EventType
@@ -27,6 +28,7 @@ def base_update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
Takes care of on_delete handling.
"""
this_fqid = fqid_from_collection_and_id(self.model.collection, instance["id"])
+ poll_ids_to_delete: list[int] = []
if self.datastore.is_to_be_deleted(this_fqid):
return instance
@@ -55,7 +57,8 @@ def base_update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
field = cast(BaseRelationField, self.model.get_field(field_name))
# Check on_delete.
# Extract all foreign keys as fqids from the model
- foreign_fqids = transform_to_fqids(value, field.get_target_collection())
+ collection_from_field = field.get_target_collection()
+ foreign_fqids = transform_to_fqids(value, collection_from_field)
if field.on_delete != OnDelete.SET_NULL:
if field.on_delete == OnDelete.PROTECT:
protected_fqids = [
@@ -65,6 +68,17 @@ def base_update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
]
if protected_fqids:
raise ProtectedModelsException(this_fqid, protected_fqids)
+ elif collection_from_field in collections_managed_by_vote:
+ if collection_from_field == "poll":
+ poll_ids_to_delete = [
+ id_
+ for id_ in value
+ if not self.is_to_be_deleted(
+ fqid_from_collection_and_id("poll", id_)
+ )
+ ]
+ for fqid in foreign_fqids:
+ self.datastore.apply_to_be_deleted(fqid)
else:
# case: field.on_delete == OnDelete.CASCADE
# Execute the delete action for all fqids
@@ -91,6 +105,8 @@ def base_update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
# Add additional relation models and execute all previously gathered delete actions
# catch all protected models exception to gather all protected fqids
all_protected_fqids: list[FullQualifiedId] = []
+ for id_ in poll_ids_to_delete:
+ self.vote_service.delete(id_)
for fqid, delete_action_class, delete_action_data in delete_actions:
try:
# Skip models that were deleted in the meantime
diff --git a/openslides_backend/models/base.py b/openslides_backend/models/base.py
index 1c06fb1d83..3fc0d6ae0c 100644
--- a/openslides_backend/models/base.py
+++ b/openslides_backend/models/base.py
@@ -10,6 +10,7 @@
from . import fields
model_registry: dict[Collection, type["Model"]] = {}
+collections_managed_by_vote: list[Collection] = []
def json_dict_to_non_json_data_types(json: dict[str, Any]) -> None:
@@ -59,6 +60,8 @@ def __new__(metaclass, class_name, class_parents, class_attributes): # type: ig
attr.own_collection = new_class.collection
attr.own_field_name = attr_name
model_registry[new_class.collection] = new_class
+ if new_class.managed_by == "vote":
+ collections_managed_by_vote.append(new_class.collection)
return new_class
diff --git a/openslides_backend/services/vote/adapter.py b/openslides_backend/services/vote/adapter.py
index 740a1cfdc9..ddbadc99c5 100644
--- a/openslides_backend/services/vote/adapter.py
+++ b/openslides_backend/services/vote/adapter.py
@@ -1,3 +1,4 @@
+from enum import StrEnum
from typing import Any, Literal
import requests
@@ -9,6 +10,11 @@
from .interface import VoteService
+class RequestMethod(StrEnum):
+ POST = "post"
+ DELETE = "delete"
+
+
class VoteAdapter(VoteService, AuthenticatedService):
"""
Adapter to connect to the vote service.
@@ -18,15 +24,20 @@ def __init__(self, vote_url: str, logging: LoggingModule) -> None:
self.url = vote_url
self.logger = logging.getLogger(__name__)
- def retrieve(self, endpoint: str, payload: dict[str, Any] | None = None) -> Any:
- response = self.make_request(endpoint, payload)
+ def retrieve(
+ self,
+ endpoint: str,
+ request_method: RequestMethod = RequestMethod.POST,
+ payload: dict[str, Any] | None = None,
+ ) -> Any:
+ response = self.make_request(endpoint, request_method, payload)
message = f"Vote service sends HTTP {response.status_code} with the following content: {str(response.content)}."
if response.status_code < 400:
self.logger.debug(message)
elif response.status_code == 500:
self.logger.error(message)
raise VoteServiceException(
- "Vote service sends HTTP 500 Internal Server Error."
+ f"Vote service sends HTTP 500 Internal Server Error with the message: {message}."
)
else:
self.logger.error(message)
@@ -34,11 +45,25 @@ def retrieve(self, endpoint: str, payload: dict[str, Any] | None = None) -> Any:
if response.content:
return response.json()
- def make_request(self, endpoint: str, payload: dict[str, Any] | None = None) -> Any:
+ def make_request(
+ self,
+ endpoint: str,
+ request_method: RequestMethod,
+ payload: dict[str, Any] | None = None,
+ ) -> Any:
if not self.access_token or not self.refresh_id:
raise VoteServiceException("You must be logged in to vote")
payload_json = json.dumps(payload, separators=(",", ":")) if payload else None
try:
+ if request_method == RequestMethod.DELETE:
+ return requests.delete(
+ url=endpoint,
+ headers={
+ "Content-Type": "application/json",
+ **self.get_auth_header(),
+ },
+ cookies=self.get_auth_cookie(),
+ )
return requests.post(
url=endpoint,
data=payload_json,
@@ -55,19 +80,19 @@ def make_request(self, endpoint: str, payload: dict[str, Any] | None = None) ->
raise VoteServiceException(f"Cannot reach the vote service on {endpoint}.")
def create(self, payload: dict[str, Any]) -> dict[str, Any]:
- endpoint = self.get_endpoint("create")
- return self.retrieve(endpoint, payload)
+ endpoint = self.get_endpoint()
+ return self.retrieve(endpoint, payload=payload)
def update(self, id: int, payload: dict[str, Any]) -> dict[str, Any]:
- endpoint = self.get_endpoint("update", id)
- return self.retrieve(endpoint, payload)
+ endpoint = self.get_endpoint(id)
+ return self.retrieve(endpoint, payload=payload)
def delete(self, id: int) -> dict[str, Any]:
- endpoint = self.get_endpoint("delete", id)
- return self.retrieve(endpoint)
+ endpoint = self.get_endpoint(id)
+ return self.retrieve(endpoint, RequestMethod.DELETE)
def start(self, id: int) -> dict[str, Any]:
- endpoint = self.get_endpoint("start", id)
+ endpoint = self.get_endpoint(id, "start")
return self.retrieve(endpoint)
def finalize(
@@ -75,14 +100,22 @@ def finalize(
id: int,
optional_attributes: list[Literal["publish", "anonymize"]] = [],
) -> dict[str, Any]:
- endpoint = self.get_endpoint("finalize", id)
+ endpoint = self.get_endpoint(id, "finalize")
if optional_attributes:
- endpoint += f"&{'&'.join(optional_attributes)}"
+ endpoint += f"?{'&'.join(optional_attributes)}"
return self.retrieve(endpoint)
def reset(self, id: int) -> dict[str, Any]:
- endpoint = self.get_endpoint("reset", id)
+ endpoint = self.get_endpoint(id, "reset")
return self.retrieve(endpoint)
- def get_endpoint(self, route: str, id: int | None = None) -> str:
- return f"{self.url}/{route}" + (f"?id={id}" if id else "")
+ def vote(self, id: int, payload: dict[str, Any]) -> dict[str, Any]:
+ endpoint = self.get_endpoint(id, "vote")
+ return self.retrieve(endpoint, payload=payload)
+
+ def get_endpoint(self, id: int | None = None, route: str | None = None) -> str:
+ return (
+ f"{self.url}/poll"
+ + (f"/{id}" if id else "")
+ + (f"/{route}" if route else "")
+ )
diff --git a/openslides_backend/services/vote/interface.py b/openslides_backend/services/vote/interface.py
index 7a468a62f0..ef69f58ad5 100644
--- a/openslides_backend/services/vote/interface.py
+++ b/openslides_backend/services/vote/interface.py
@@ -28,3 +28,6 @@ def finalize(
@abstractmethod
def reset(self, id: int) -> dict[str, Any]: ...
+
+ @abstractmethod
+ def vote(self, id: int, payload: dict[str, Any]) -> dict[str, Any]: ...
diff --git a/tests/system/action/meeting/test_delete.py b/tests/system/action/meeting/test_delete.py
index 55c7131bdb..9aa2c0b030 100644
--- a/tests/system/action/meeting/test_delete.py
+++ b/tests/system/action/meeting/test_delete.py
@@ -437,10 +437,15 @@ def test_delete_full_meeting(self) -> None:
self.assert_model_not_exists(f"motion_workflow/{i+1}")
for i in range(5):
self.assert_model_not_exists(f"poll/{i+1}")
- for i in range(13):
- self.assert_model_not_exists(f"option/{i+1}")
+ for i in range(10):
+ self.assert_model_not_exists(f"poll_option/{i+1}")
for i in range(9):
- self.assert_model_not_exists(f"vote/{i+1}")
+ self.assert_model_not_exists(f"poll_ballot/{i+1}")
+ for i in range(2):
+ self.assert_model_not_exists(f"poll_config_approval/{i+1}")
+ self.assert_model_not_exists("poll_config_selection/1")
+ self.assert_model_not_exists("poll_config_rating_score/1")
+ self.assert_model_not_exists("poll_config_rating_approval/1")
for i in range(2):
self.assert_model_not_exists(f"assignment/{i+1}")
for i in range(5):
diff --git a/tests/system/action/user/test_merge_together.py b/tests/system/action/user/test_merge_together.py
index 0c71749ca2..6e502e55ad 100644
--- a/tests/system/action/user/test_merge_together.py
+++ b/tests/system/action/user/test_merge_together.py
@@ -5,6 +5,7 @@
from typing import Any, Literal, cast
from zoneinfo import ZoneInfo
+import pytest
from psycopg.types.json import Jsonb
from openslides_backend.action.actions.speaker.speech_state import SpeechState
@@ -17,11 +18,13 @@
fqid_from_collection_and_id,
)
from openslides_backend.shared.util import ONE_ORGANIZATION_ID
-from tests.system.action.poll.test_vote import BaseVoteTestCase
+from tests.system.action.base import BaseActionTestCase
from tests.util import Response
-class UserMergeTogether(BaseVoteTestCase):
+@pytest.mark.skip()
+# class UserMergeTogether(BaseVoteTestCase):
+class UserMergeTogether(BaseActionTestCase):
"""committee/63 is created but remains unused in all of the tests as 60 is used for meeting/1 and 4"""
def setUp(self) -> None:
@@ -849,7 +852,6 @@ def set_up_polls_for_merge(self) -> None:
"meeting/10": {"present_user_ids": [5]},
"meeting_user/15": {"vote_delegated_to_id": 14},
"motion_state/4": {"allow_create_poll": True},
- "motion_state/4": {"allow_create_poll": True},
"motion_submitter/1": {
"id": 1,
"weight": 1,
From 3eab0ffd678f652b68dceb188a08204f2bc656a2 Mon Sep 17 00:00:00 2001
From: vkrasnovyd
Date: Mon, 1 Jun 2026 15:42:16 +0200
Subject: [PATCH 12/23] Update meta hash
---
meta | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/meta b/meta
index 0ffd96331f..d9cc01053a 160000
--- a/meta
+++ b/meta
@@ -1 +1 @@
-Subproject commit 0ffd96331f81d6e0a671fa591bcf3de6bfa71a91
+Subproject commit d9cc01053a0f230a312df6100fd6dd0dff17d2c9
From e2897fbf3215e4280665fbc7aa80503edeedd97e Mon Sep 17 00:00:00 2001
From: Viktoriia Krasnovyd <114735598+vkrasnovyd@users.noreply.github.com>
Date: Thu, 4 Jun 2026 10:13:28 +0200
Subject: [PATCH 13/23] [vote] Update user.merge_together (#3599)
---
.../action/actions/user/base_merge_mixin.py | 3 +-
.../action/actions/user/merge_mixins.py | 145 ++++--
.../action/actions/user/merge_together.py | 226 +--------
openslides_backend/models/mixins.py | 13 +-
.../system/action/user/test_merge_together.py | 457 +++++++-----------
5 files changed, 306 insertions(+), 538 deletions(-)
diff --git a/openslides_backend/action/actions/user/base_merge_mixin.py b/openslides_backend/action/actions/user/base_merge_mixin.py
index 0d72a87d1f..fb56162a16 100644
--- a/openslides_backend/action/actions/user/base_merge_mixin.py
+++ b/openslides_backend/action/actions/user/base_merge_mixin.py
@@ -98,7 +98,8 @@ def add_collection_field_groups(
self._all_collection_fields[collection] = [
i
for i in Class.__dict__.keys()
- if i[:1] != "_" and i not in ["collection", "verbose_name", "id"]
+ if i[:1] != "_"
+ and i not in ["collection", "verbose_name", "id", "managed_by"]
]
if back_field:
self._collection_back_fields[collection] = back_field
diff --git a/openslides_backend/action/actions/user/merge_mixins.py b/openslides_backend/action/actions/user/merge_mixins.py
index 13ea1622cc..e47fce259e 100644
--- a/openslides_backend/action/actions/user/merge_mixins.py
+++ b/openslides_backend/action/actions/user/merge_mixins.py
@@ -11,10 +11,9 @@
MotionWorkingGroupSpeaker,
PersonalNote,
Poll,
- PollBallot,
- PollOption,
Speaker,
)
+from ....services.database.commands import GetManyRequest
from ....shared.exceptions import ActionException
from ....shared.filters import And, FilterOperator, Or
from ....shared.patterns import Collection, fqid_from_collection_and_id
@@ -115,7 +114,6 @@ def get_full_history_information(self) -> HistoryInformation | None:
class MotionSubmitterMergeMixin(BaseMergeMixin):
- # ???
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.add_collection_field_groups(
@@ -197,36 +195,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
)
-class PollMergeMixin(BaseMergeMixin):
- def __init__(self, *args: Any, **kwargs: Any) -> None:
- super().__init__(*args, **kwargs)
- self.add_collection_field_groups(
- Poll,
- {},
- "meeting_user_id",
- )
-
-
-class PollOptionMergeMixin(BaseMergeMixin):
- def __init__(self, *args: Any, **kwargs: Any) -> None:
- super().__init__(*args, **kwargs)
- self.add_collection_field_groups(
- PollOption,
- {},
- "meeting_user_id",
- )
-
-
-class PollBallotMergeMixin(BaseMergeMixin):
- def __init__(self, *args: Any, **kwargs: Any) -> None:
- super().__init__(*args, **kwargs)
- self.add_collection_field_groups(
- PollBallot,
- {},
- "meeting_user_id",
- )
-
-
class MeetingUserMergeMixin(
PersonalNoteMergeMixin,
MotionWorkingGroupSpeakerMergeMixin,
@@ -234,9 +202,6 @@ class MeetingUserMergeMixin(
MotionSubmitterMergeMixin,
MotionSupporterMergeMixin,
AssignmentCandidateMergeMixin,
- PollMergeMixin,
- PollBallotMergeMixin,
- PollOptionMergeMixin,
SpeakerMergeMixin,
):
def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -261,10 +226,10 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
"chat_message_ids",
"group_ids",
"structure_level_ids",
- "poll_voted_ids",
- "poll_option_ids",
- "acting_ballot_ids",
- "represented_ballot_ids",
+ "poll_voted_ids", # throw error if conflict on same poll
+ "poll_option_ids", # throw error if conflict on same poll
+ "acting_ballot_ids", # throw error if conflict on same poll
+ "represented_ballot_ids", # throw error if conflict on same poll
],
"deep_merge": {
"assignment_candidate_ids": "assignment_candidate",
@@ -315,11 +280,37 @@ def get_merge_comparison_hash(
case _:
return super().get_merge_comparison_hash(collection, model)
- def check_polls_helper(self, meeting_users: dict[str, dict[str, Any]]) -> list[str]:
+ def check_polls(self, into: PartialModel, other_models: list[PartialModel]) -> None:
messages: list[str] = []
group_ids: set[int] = set()
meeting_ids: set[int] = set()
meeting_id_by_group_ids: dict[int, int] = {}
+
+ meeting_user_ids = [
+ mu_id
+ for user in [into, *other_models]
+ for mu_id in user.get("meeting_user_ids", [])
+ ]
+ meeting_users = self.datastore.get_many(
+ [
+ GetManyRequest(
+ "meeting_user",
+ meeting_user_ids,
+ [
+ "user_id",
+ "meeting_id",
+ "group_ids",
+ "vote_delegations_from_ids",
+ "vote_delegated_to_id",
+ "poll_voted_ids",
+ "poll_option_ids",
+ "acting_ballot_ids",
+ "represented_ballot_ids",
+ ],
+ )
+ ]
+ )["meeting_user"]
+
for m_user in meeting_users.values():
if len(g_ids := m_user.get("group_ids", [])):
meeting_id_by_group_ids.update(
@@ -396,4 +387,74 @@ def check_polls_helper(self, meeting_users: dict[str, dict[str, Any]]) -> list[s
messages.append(
f"some of the selected users are delegating votes to each other in meeting(s) {', '.join(bad_meetings)}"
)
- return messages
+
+ ballot_poll_ids_per_user_id: dict[int, set[int]] = {}
+ option_poll_ids_per_user_id: dict[int, set[int]] = {}
+ for meeting_user in meeting_users.values():
+ if poll_voted_ids := meeting_user.get("poll_voted_ids"):
+ ballot_poll_ids_per_user_id.setdefault(
+ meeting_user["user_id"], set()
+ ).update(set(poll_voted_ids))
+ if len(
+ (o_ids := meeting_user.get("poll_option_ids", []))
+ + (
+ b_ids := list(
+ {
+ id_
+ for id_ in [
+ *meeting_user.get("acting_ballot_ids", []),
+ *meeting_user.get("represented_ballot_ids", []),
+ ]
+ }
+ )
+ )
+ ):
+ many_models = self.datastore.get_many(
+ [
+ GetManyRequest("poll_option", o_ids, ["poll_id"]),
+ GetManyRequest("poll_ballot", b_ids, ["poll_id"]),
+ ]
+ )
+ if o_ids:
+ option_poll_ids_per_user_id.setdefault(
+ meeting_user["user_id"], set()
+ ).update(
+ {
+ option["poll_id"]
+ for option in many_models["poll_option"].values()
+ }
+ )
+ ballot_data = many_models["poll_ballot"]
+ ballot_poll_ids_per_user_id.setdefault(
+ meeting_user["user_id"], set()
+ ).update({ballot["poll_id"] for ballot in ballot_data.values()})
+ ballot_conflicts = self._get_conflicts_between_users(
+ ballot_poll_ids_per_user_id
+ )
+ option_conflicts = self._get_conflicts_between_users(
+ option_poll_ids_per_user_id
+ )
+ if len(ballot_conflicts):
+ messages.append(
+ f"among the selected users multiple voted in poll(s) {', '.join([str(id_) for id_ in ballot_conflicts])}"
+ )
+ if len(option_conflicts):
+ messages.append(
+ f"multiple of the selected users are among the options in poll(s) {', '.join([str(id_) for id_ in option_conflicts])}"
+ )
+
+ if len(messages):
+ raise ActionException(
+ f"Cannot carry out merge into user/{into['id']}, because {' and '.join(messages)}"
+ )
+
+ def _get_conflicts_between_users(self, ids_map: dict[int, set[int]]) -> set[int]:
+ seen_ids = set()
+ duplicates = set()
+ for ids in ids_map.values():
+ for id_ in ids:
+ if id_ in seen_ids:
+ duplicates.add(id_)
+ else:
+ seen_ids.add(id_)
+ return duplicates
diff --git a/openslides_backend/action/actions/user/merge_together.py b/openslides_backend/action/actions/user/merge_together.py
index 74daecc84a..5f690b0235 100644
--- a/openslides_backend/action/actions/user/merge_together.py
+++ b/openslides_backend/action/actions/user/merge_together.py
@@ -1,6 +1,5 @@
-from typing import Any, cast
+from typing import Any
-from openslides_backend.models.models import Poll
from openslides_backend.services.database.interface import PartialModel
from ....action.mixins.archived_meeting_check_mixin import CheckForArchivedMeetingMixin
@@ -360,198 +359,6 @@ def call_other_actions(
[{"id": id_} for id_ in to_delete],
)
- def check_polls(self, into: PartialModel, other_models: list[PartialModel]) -> None:
- all_models = [into, *other_models]
- # user -> meeting user, same name
- mu_ids_per_user = {
- user["id"]: user.get("meeting_user_ids", []) for user in all_models
- }
- meeting_user_ids = [
- mu_id for user in all_models for mu_id in user.get("meeting_user_ids", [])
- ]
- poll_conflict_fields = [
- "poll_voted_ids",
- "poll_option_ids",
- "acting_ballot_ids",
- "represented_ballot_ids",
- ]
- helper_fields = [
- "vote_delegations_from_ids",
- "vote_delegated_to_id",
- "meeting_id",
- "group_ids",
- ]
- mu_data = self.datastore.get_many(
- [
- GetManyRequest(
- "meeting_user",
- meeting_user_ids,
- ["user_id"] + poll_conflict_fields + helper_fields,
- )
- ]
- )["meeting_user"]
-
- vote_poll_ids_per_meeting_user_id: dict[int, set[int]] = {
- meeting_user["id"]: {
- "user_id": meeting_user["user_id"],
- "poll_voted_ids": poll_voted_ids,
- }
- for id_, meeting_user in mu_data.items()
- if (poll_voted_ids := meeting_user.get("poll_voted_ids"))
- }
-
- ballot_poll_ids_per_user_id: dict[int, set[int]] = {}
- for meeting_user in vote_poll_ids_per_meeting_user_id.values():
- ballot_poll_ids_per_user_id.setdefault(
- meeting_user["user_id"], set()
- ).update(set(meeting_user["poll_voted_ids"]))
- # acting_vote_ids, represented_vote_ids -> mu
- # meeting_user.poll_option_ids
- # list: option of config-approval with options
- option_poll_ids_per_user_id: dict[int, set[int]] = {}
- candidate_list_ids_per_user_id: dict[int, set[int]] = {}
- meeting_user_ids: list[int] = []
- for model in mu_data.values():
- if len(
- # (pc_ids := model.get("poll_candidate_ids", []))
- # +(o_ids := model.get("poll_option_ids", []))
- (o_ids := model.get("poll_option_ids", []))
- + (
- v_ids := list(
- {
- id_
- for id_ in [
- *model.get("acting_ballot_ids", []),
- *model.get("represented_ballot_ids", []),
- ]
- }
- )
- )
- ):
- get_many_requests = [
- # GetManyRequest(
- # "poll_candidate", pc_ids, ["poll_candidate_list_id"]
- # ),
- GetManyRequest(
- "poll_config_option",
- o_ids,
- ["poll_config_id"],
- ),
- GetManyRequest(
- "poll_ballot",
- v_ids,
- ["poll_id"],
- ),
- ]
- many_models = self.datastore.get_many(get_many_requests)
- # if pc_ids:
- # candidate_list_ids_per_user_id[model["id"]] = {
- # poll_candidate["poll_candidate_list_id"]
- # for poll_candidate in many_models["poll_candidate"].values()
- # if poll_candidate.get("poll_candidate_list_id")
- # }
- # if o_ids:
- # option_poll_ids_per_user_id[model["id"]] = {
- # option["poll_id"]
- # for option in many_models["option"].values()
- # if option.get("poll_id")
- # }
- if o_ids:
- option_data = many_models["poll_config_option"]
- config_ids_per_collection = {}
- for config in option_data.values():
- collection, id_ = config["poll_config_id"].split("/")
- config_ids_per_collection.setdefault(collection, set()).update(
- id_
- )
- gmr = [
- GetManyRequest(
- collection,
- [int(id_) for id_ in ids],
- ["poll_id"],
- )
- for collection, ids in config_ids_per_collection.items()
- ]
- r = self.datastore.get_many(gmr)
- option_poll_ids_per_user_id[model["user_id"]] = {
- *option_poll_ids_per_user_id.get(model["user_id"], set()),
- *{
- cast(int, option["poll_id"])
- for collection, config in self.datastore.get_many(
- gmr
- ).items()
- for option in config.values()
- if collection != Poll.CONFIG_TYPE_APPROVAL
- },
- }
- candidate_list_ids_per_user_id[model["user_id"]] = {
- *candidate_list_ids_per_user_id.get(model["user_id"], set()),
- *{
- cast(int, option["poll_id"])
- for collection, config in self.datastore.get_many(
- gmr
- ).items()
- for option in config.values()
- if collection == Poll.CONFIG_TYPE_APPROVAL
- },
- }
-
- ballot_data = many_models["poll_ballot"]
- ballot_poll_ids_per_user_id[model["user_id"]] = {
- *ballot_poll_ids_per_user_id.get(model["user_id"], set()),
- *{
- id_
- for id_ in [
- ballot["poll_id"] for ballot in ballot_data.values()
- ]
- },
- }
- # if v_ids:
- # vote_poll_ids_per_user_id[model["id"]] = {
- # vote["poll_id"]
- # for vote in many_models["vote"].values()
- # if vote.get("poll_id")
- # }
- # meeting_user_ids += model.get("meeting_user_ids", [])
- voting_conflicts = {
- poll_id
- for id1, poll_ids1 in ballot_poll_ids_per_user_id.items()
- for id2, poll_ids2 in ballot_poll_ids_per_user_id.items()
- for poll_id in poll_ids1.intersection(poll_ids2)
- if id1 != id2
- }
- option_conflicts = {
- poll_id
- for id1, poll_ids1 in option_poll_ids_per_user_id.items()
- for id2, poll_ids2 in option_poll_ids_per_user_id.items()
- for poll_id in poll_ids1.intersection(poll_ids2)
- if id1 != id2
- }
- candidate_list_conflicts = {
- list_id
- for id1, list_ids1 in candidate_list_ids_per_user_id.items()
- for id2, list_ids2 in candidate_list_ids_per_user_id.items()
- for list_id in list_ids1.intersection(list_ids2)
- if id1 != id2
- }
- messages: list[str] = self.check_polls_helper(mu_data)
- if len(voting_conflicts):
- messages.append(
- f"among the selected users multiple voted in poll(s) {', '.join([str(id_) for id_ in voting_conflicts])}"
- )
- if len(option_conflicts):
- messages.append(
- f"multiple of the selected users are among the options in poll(s) {', '.join([str(id_) for id_ in option_conflicts])}"
- )
- if len(candidate_list_conflicts):
- messages.append(
- f"multiple of the selected users are in the same candidate list in poll(s) {', '.join([str(id_) for id_ in candidate_list_conflicts])}"
- )
- if len(messages):
- raise ActionException(
- f"Cannot carry out merge into user/{into['id']}, because {' and '.join(messages)}"
- )
-
def get_merge_comparison_hash(
self, collection: Collection, model: PartialModel
) -> int | str | tuple[int | str, ...]:
@@ -588,9 +395,10 @@ def handle_special_field(
)
return None
case "is_present_in_meeting_ids":
- all_meeting_ids = self.get_meeting_ids_per_user(
- [into_, *ranked_others]
- )
+ all_meeting_ids = {
+ user["id"]: set(user.get("meeting_ids", []))
+ for user in [into_, *ranked_others]
+ }
check_meeting_ids = all_meeting_ids[into_["id"]]
present_meeting_ids: set[int] = set(into_.get(field, []))
for other in ranked_others:
@@ -621,30 +429,6 @@ def handle_special_field(
collection, field, into_, ranked_others, update_operations
)
- def get_meeting_ids_per_user(
- self, users: list[PartialModel]
- ) -> dict[int, set[int]]:
- meeting_users = self.datastore.filter(
- "meeting_user",
- And(
- FilterOperator("group_ids", "!=", []),
- Or(FilterOperator("user_id", "=", user["id"]) for user in users),
- ),
- ["meeting_id"],
- )
- return {
- user["id"]: {
- meeting_id
- for meeting_user_id in user.get("meeting_user_ids", [])
- if (
- meeting_id := meeting_users.get(meeting_user_id, {}).get(
- "meeting_id"
- )
- )
- }
- for user in users
- }
-
def get_full_history_information(self) -> HistoryInformation | None:
information = super().get_full_history_information() or {}
for data, ids, is_transfer in self._history_replacement_groups["user"]:
diff --git a/openslides_backend/models/mixins.py b/openslides_backend/models/mixins.py
index 01afc8729e..81ffca1633 100644
--- a/openslides_backend/models/mixins.py
+++ b/openslides_backend/models/mixins.py
@@ -84,15 +84,20 @@ class PollModelMixin:
VISIBILITY_OPEN = "open"
VISIBILITY_SECRET = "secret"
+ METHOD_APPROVAL = "approval"
+ METHOD_SELECTION = "selection"
+ METHOD_RATING_SCORE = "rating_score"
+ METHOD_RATING_APPROVAL = "rating_approval"
+ METHOD_RATING_STV_SCOTTISH = "stv_scottish"
+
CONFIG_TYPE_APPROVAL = "poll_config_approval"
CONFIG_TYPE_SELECTION = "poll_config_selection"
CONFIG_TYPE_RATING_SCORE = "poll_config_rating_score"
CONFIG_TYPE_RATING_APPROVAL = "poll_config_rating_approval"
+ CONFIG_TYPE_RATING_STV_SCOTTISH = "poll_config_stv_scottish"
- ONEHUNDRED_PERCENT_BASE_Y = "Y"
- ONEHUNDRED_PERCENT_BASE_YN = "YN"
- ONEHUNDRED_PERCENT_BASE_YNA = "YNA"
- ONEHUNDRED_PERCENT_BASE_N = "N"
+ ONEHUNDRED_PERCENT_BASE_NO_GENERAL = "no_general"
+ ONEHUNDRED_PERCENT_BASE_YES_NO = "yes_no"
ONEHUNDRED_PERCENT_BASE_VALID = "valid"
ONEHUNDRED_PERCENT_BASE_CAST = "cast"
ONEHUNDRED_PERCENT_BASE_ENTITLED = "entitled"
diff --git a/tests/system/action/user/test_merge_together.py b/tests/system/action/user/test_merge_together.py
index 6e502e55ad..c55de8b324 100644
--- a/tests/system/action/user/test_merge_together.py
+++ b/tests/system/action/user/test_merge_together.py
@@ -5,9 +5,6 @@
from typing import Any, Literal, cast
from zoneinfo import ZoneInfo
-import pytest
-from psycopg.types.json import Jsonb
-
from openslides_backend.action.actions.speaker.speech_state import SpeechState
from openslides_backend.action.relations.relation_manager import RelationManager
from openslides_backend.action.util.actions_map import actions_map
@@ -22,8 +19,6 @@
from tests.util import Response
-@pytest.mark.skip()
-# class UserMergeTogether(BaseVoteTestCase):
class UserMergeTogether(BaseActionTestCase):
"""committee/63 is created but remains unused in all of the tests as 60 is used for meeting/1 and 4"""
@@ -841,9 +836,6 @@ def set_up_polls_for_merge(self) -> None:
self.create_assignment(1, 1)
self.create_motion(4, 4)
self.create_topic(7, 7)
- self.create_assignment(1, 1)
- self.create_motion(4, 1)
- self.create_topic(7, 7)
self.set_models(
{
"meeting/1": {"present_user_ids": [2, 4]},
@@ -859,213 +851,133 @@ def set_up_polls_for_merge(self) -> None:
"meeting_id": 4,
"meeting_user_id": 43,
},
- }
- )
- polls_data = {
- "poll/1": {
- "title": "Assignment poll 1",
- "content_object_id": "assignment/1",
- "visibility": Poll.VISIBILITY_NAMED,
- "config_id": "poll_config_selection/1",
- "state": Poll.STATE_STARTED,
- "meeting_id": 1,
- "entitled_group_ids": [1, 2, 3],
- },
- "poll/2": {
- "title": "Assignment poll 2",
- "content_object_id": "assignment/1",
- "visibility": Poll.VISIBILITY_NAMED,
- "config_id": "poll_config_approval/2",
- "state": Poll.STATE_STARTED,
- "meeting_id": 1,
- "entitled_group_ids": [1, 2, 3],
- },
- "poll/3": {
- "title": "Assignment poll 3",
- "content_object_id": "assignment/1",
- "visibility": Poll.VISIBILITY_NAMED,
- "config_id": "poll_config_approval/3",
- "state": Poll.STATE_STARTED,
- "meeting_id": 1,
- "entitled_group_ids": [1, 2, 3],
- },
- "poll/4": {
- "title": "Assignment poll 4",
- "content_object_id": "assignment/1",
- "visibility": Poll.VISIBILITY_SECRET,
- "config_id": "poll_config_selection/4",
- "state": Poll.STATE_STARTED,
- "meeting_id": 1,
- "entitled_group_ids": [1, 2, 3],
- },
- "poll/5": {
- "title": "Motion poll",
- "content_object_id": "motion/1",
- "visibility": Poll.VISIBILITY_NAMED,
- "config_id": "poll_config_approval/5",
- "state": Poll.STATE_STARTED,
- "meeting_id": 4,
- "entitled_group_ids": [4, 5, 6],
- },
- "poll/6": {
- "title": "Topic poll",
- "content_object_id": "topic/7",
- "visibility": Poll.VISIBILITY_SECRET,
- "config_id": "poll_config_rating_score/6",
- "state": Poll.STATE_STARTED,
- "meeting_id": 7,
- "entitled_group_ids": [7, 8, 9],
- },
- }
- # add entitled groups
- configs_data = {
- **{
- f"group/{group_id}": {"poll_ids": list(range(1, 5))}
- for group_id in range(1, 4)
- },
- "poll_config_selection/1": {
- "poll_id": 1,
- "min_options_amount": 1,
- "max_options_amount": 2,
- },
- "poll_config_option/112": {
- "poll_config_id": "poll_config_selection/1",
- "meeting_user_id": 12,
- },
- "poll_config_option/115": {
- "poll_config_id": "poll_config_selection/1",
- "meeting_user_id": 15,
- },
- "poll_config_approval/2": {"poll_id": 2, "allow_abstain": False},
- "poll_config_option/214": {
- "poll_config_id": "poll_config_approval/2",
- "meeting_user_id": 14,
- },
- "poll_config_option/215": {
- "poll_config_id": "poll_config_approval/2",
- "meeting_user_id": 15,
- },
- "poll_config_approval/3": {"poll_id": 3, "allow_abstain": False},
- "poll_config_option/312": {
- "poll_config_id": "poll_config_approval/3",
- "meeting_user_id": 12,
- },
- "poll_config_option/315": {
- "poll_config_id": "poll_config_approval/3",
- "meeting_user_id": 15,
- },
- "poll_config_selection/4": {
- "poll_id": 4,
- "min_options_amount": 1,
- "max_options_amount": 2,
- },
- "poll_config_option/414": {
- "poll_config_id": "poll_config_selection/4",
- "meeting_user_id": 14,
- },
- "poll_config_option/415": {
- "poll_config_id": "poll_config_selection/4",
- "meeting_user_id": 15,
- },
- "poll_config_approval/5": {"poll_id": 5},
- "poll_config_rating_score/6": {
- "poll_id": 6,
- "min_options_amount": 1,
- "max_options_amount": 3,
- "max_votes_per_option": 1,
- },
- **{
- f"poll_config_option/{poll_6_option_id}": {
- "poll_config_id": "poll_config_rating_score/6",
- "text": f"Option {poll_6_option_id}",
- }
- for poll_6_option_id in range(61, 64)
- },
- "meeting_user/13": {
- "user_id": 3,
- "meeting_id": 1,
- "group_ids": [1],
- "vote_weight": Decimal("1"),
- },
- }
- self.set_models(polls_data)
- self.set_models(configs_data)
-
- # def vote(self, poll_id: int, payload: dict[str, Any]) -> None:
- # self.vote_service.vote(poll_id, payload)
- def vote(
- self,
- base: int,
- poll_id: int,
- value: Any, # TODO: narrow down
- acting_meeting_user_id: int | None = None,
- represented_meeting_user_id: int | None = None,
- is_secret: bool = False,
- ) -> None:
- mu_data = (
- {
- "acting_meeting_user_id": acting_meeting_user_id,
- "represented_meeting_user_id": represented_meeting_user_id
- or acting_meeting_user_id,
- }
- if not is_secret
- else {}
- )
- self.set_models(
- {
- f"ballot/{base}": {
- "poll_id": poll_id,
- "value": value,
- **mu_data,
- }
+ "poll/1": {
+ "title": "Assignment poll 1",
+ "content_object_id": "assignment/1",
+ "visibility": Poll.VISIBILITY_NAMED,
+ "config_id": "poll_config_approval/1",
+ "meeting_id": 1,
+ "state": Poll.STATE_FINISHED,
+ },
+ "poll_config_approval/1": {
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID
+ },
+ "poll_option/1": {"poll_id": 1, "meeting_user_id": 12},
+ "poll_option/2": {"poll_id": 1, "meeting_user_id": 15},
+ "poll/2": {
+ "title": "Assignment poll 2",
+ "content_object_id": "assignment/1",
+ "visibility": Poll.VISIBILITY_NAMED,
+ "config_id": "poll_config_selection/1",
+ "meeting_id": 1,
+ "state": Poll.STATE_FINISHED,
+ },
+ "poll_config_selection/1": {
+ "min_options_amount": 1,
+ "max_options_amount": 1,
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID,
+ },
+ "poll_option/3": {"poll_id": 2, "meeting_user_id": 15},
+ "poll_option/4": {"poll_id": 2, "meeting_user_id": 14},
+ "poll/3": {
+ "title": "Assignment poll 3",
+ "content_object_id": "assignment/1",
+ "visibility": Poll.VISIBILITY_NAMED,
+ "config_id": "poll_config_approval/2",
+ "meeting_id": 1,
+ "state": Poll.STATE_FINISHED,
+ },
+ "poll_config_approval/2": {
+ "allow_abstain": False,
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID,
+ },
+ "poll_option/5": {"poll_id": 3, "meeting_user_id": 12},
+ "poll_option/6": {"poll_id": 3, "meeting_user_id": 15},
+ "poll/4": {
+ "title": "Assignment poll 4",
+ "content_object_id": "assignment/1",
+ "visibility": Poll.VISIBILITY_SECRET,
+ "config_id": "poll_config_approval/3",
+ "meeting_id": 1,
+ "state": Poll.STATE_FINISHED,
+ },
+ "poll_config_approval/3": {
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID
+ },
+ "poll_option/7": {"poll_id": 4, "meeting_user_id": 14},
+ "poll_option/8": {"poll_id": 4, "meeting_user_id": 15},
+ "poll/5": {
+ "title": "Motion poll",
+ "content_object_id": "motion/4",
+ "visibility": Poll.VISIBILITY_NAMED,
+ "config_id": "poll_config_approval/4",
+ "meeting_id": 4,
+ "state": Poll.STATE_FINISHED,
+ },
+ "poll_config_approval/4": {
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID
+ },
+ "poll/6": {
+ "title": "Topic poll",
+ "content_object_id": "topic/7",
+ "visibility": Poll.VISIBILITY_SECRET,
+ "config_id": "poll_config_rating_score/1",
+ "meeting_id": 7,
+ "state": Poll.STATE_FINISHED,
+ "entitled_group_ids": [7, 8, 9],
+ },
+ "poll_config_rating_score/1": {
+ "min_options_amount": 1,
+ "max_options_amount": 3,
+ "max_votes_per_option": 1,
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID,
+ },
+ "poll_option/9": {"poll_id": 6, "text": "Option 1"},
+ "poll_option/10": {"poll_id": 6, "text": "Option 2"},
+ "poll_option/11": {"poll_id": 6, "text": "Option 3"},
+ **{f"group/{id_}": {"poll_ids": [1, 2, 3, 4]} for id_ in range(1, 4)},
+ **{f"group/{id_}": {"poll_ids": [5]} for id_ in range(4, 7)},
+ **{f"group/{id_}": {"poll_ids": [6]} for id_ in range(7, 10)},
}
)
- def create_polls_with_correct_votes(self, poll_4_is_running: bool = False) -> None:
+ def create_polls_with_correct_votes(self) -> None:
self.set_up_polls_for_merge()
- self.vote(1, 1, [112, 115], 14)
- self.vote(2, 1, [112], 14, 15)
- self.vote(3, 2, "yes", 12)
- # secret
- self.vote(4, 5, "abstain", 43, is_secret=True)
- self.vote(5, 6, Jsonb({"1": 1, "2": 1, "3": 0}), 73, is_secret=True)
-
- self.login(1)
self.set_models(
{
"meeting_user/12": {"poll_voted_ids": [2]},
- "meeting_user/43": {"poll_voted_ids": [5]},
- "meeting_user/73": {"poll_voted_ids": [6]},
"meeting_user/14": {"poll_voted_ids": [1]},
"meeting_user/15": {"poll_voted_ids": [1]},
+ "meeting_user/43": {"poll_voted_ids": [5]},
+ "meeting_user/73": {"poll_voted_ids": [6]},
+ "poll_ballot/1": {
+ "value": "no",
+ "poll_id": 1,
+ "acting_meeting_user_id": 14,
+ "represented_meeting_user_id": 14,
+ },
+ "poll_ballot/2": {
+ "value": "no",
+ "poll_id": 1,
+ "acting_meeting_user_id": 14,
+ "represented_meeting_user_id": 15,
+ },
+ "poll_ballot/3": {
+ "value": "4",
+ "poll_id": 2,
+ "acting_meeting_user_id": 12,
+ "represented_meeting_user_id": 12,
+ },
+ "poll_ballot/4": {
+ "value": "abstain",
+ "poll_id": 5,
+ "acting_meeting_user_id": 43,
+ "represented_meeting_user_id": 43,
+ },
+ "poll_ballot/5": {"value": "9", "poll_id": 6},
+ "poll_ballot/6": {"value": "10", "poll_id": 6},
}
)
- self.set_models(
- {
- f"poll/{poll_id}": {"state": Poll.STATE_FINISHED, "published": True}
- for poll_id in range(1, 7)
- if not (poll_id == 4 and poll_4_is_running)
- }
- )
- # self.login(4)
- # self.vote(1, {"value": [112, 115]})
- # self.vote(1, {"value": [112], "meeting_user_id": 15})
-
- # self.login(2)
- # self.vote(2, {"value": "yes"})
-
- # self.login(3)
- # self.vote(5, {"value": "abstain"})
- # self.vote(6, {"value": {"1": 1, "2": 1, "3": 0}})
-
- # self.login(1)
- # self.set_models(
- # {
- # f"poll/{poll_id}": {"state": Poll.STATE_FINISHED, "published": True}
- # for poll_id in range(1, 7)
- # if not (poll_id == 4 and poll_4_is_running)
- # }
- # )
def assert_merge_with_polls_correct(
self, password: str, add_to_creatable_ids: int = 0
@@ -1085,100 +997,76 @@ def assert_merge_with_polls_correct(
)
self.assert_model_exists("committee/60", {"user_ids": [2, 5]})
self.assert_model_exists("committee/66", {"user_ids": [2]})
+ for id_ in range(3, 5):
+ self.assert_model_not_exists(f"user/{id_}")
+ for id_ in [43, 73, 14, 44, 74, *range(106, 106 + add_to_creatable_ids)]:
+ self.assert_model_not_exists(f"meeting_user/{id_}")
self.assert_model_exists(
"meeting_user/12",
{
- "poll_option_ids": [112, 214, 312, 414],
+ "user_id": 2,
+ "meeting_id": 1,
+ "poll_option_ids": [1, 4, 5, 7],
"poll_voted_ids": [1, 2],
"acting_ballot_ids": [1, 2, 3],
"represented_ballot_ids": [1, 3],
- "vote_delegations_from_ids": [15],
},
)
-
- for id_ in range(3, 5):
- self.assert_model_not_exists(f"user/{id_}")
- for id_ in [43, 73, 14, 44, 74, *range(106, 106 + add_to_creatable_ids)]:
- self.assert_model_not_exists(f"meeting_user/{id_}")
- for meeting_id, id_ in {1: 12, 7: 106 + add_to_creatable_ids}.items():
- self.assert_model_exists(
- f"meeting_user/{id_}", {"user_id": 2, "meeting_id": meeting_id}
- )
self.assert_model_exists(
"meeting_user/42",
{
"user_id": 2,
"meeting_id": 4,
- "motion_submitter_ids": [2],
- "poll_option_ids": None,
+ "motion_submitter_ids": [1],
"poll_voted_ids": [5],
- "acting_ballot_ids": None,
- "represented_ballot_ids": None,
+ "acting_ballot_ids": [4],
+ "represented_ballot_ids": [4],
},
)
self.assert_model_exists(
- "meeting_user/106",
- {
- "user_id": 2,
- "meeting_id": 7,
- "motion_submitter_ids": None,
- "poll_option_ids": None,
- "poll_voted_ids": [6],
- "acting_ballot_ids": None,
- "represented_ballot_ids": None,
- },
+ f"meeting_user/{106 + add_to_creatable_ids}",
+ {"user_id": 2, "meeting_id": 7, "poll_voted_ids": [6]},
)
self.assert_model_not_exists("motion_submitter/2")
self.assert_model_exists(
"motion_submitter/1",
{"motion_id": 4, "meeting_user_id": 42, "meeting_id": 4, "weight": 1},
)
- for id_ in [1, 3]:
+ for poll_option_id in [1, 4, 5, 7]:
self.assert_model_exists(
- f"ballot/{id_}",
- {"acting_meeting_user_id": 12, "represented_meeting_user_id": 12},
+ f"poll_option/{poll_option_id}", {"meeting_user_id": 12}
)
+
self.assert_model_exists(
- "ballot/2",
+ "poll_ballot/2",
{"acting_meeting_user_id": 12, "represented_meeting_user_id": 15},
)
- for id_ in [4, 5]: # votes from secret voting
+ for id_ in [1, 3]:
self.assert_model_exists(
- f"ballot/{id_}",
- {"acting_meeting_user_id": None, "represented_meeting_user_id": None},
+ f"poll_ballot/{id_}",
+ {"acting_meeting_user_id": 12, "represented_meeting_user_id": 12},
)
-
- for id_ in [112, 214, 312, 414]:
+ self.assert_model_exists(
+ "poll_ballot/4",
+ {"acting_meeting_user_id": 42, "represented_meeting_user_id": 42},
+ )
+ for id_ in [5, 6]: # anonymous ballots
self.assert_model_exists(
- f"poll_config_option/{id_}", {"meeting_user_id": 12}
- )
-
- def build_expected_user_dates(
- voted_present_user_delegated_merged: list[
- tuple[bool, bool, int, int | None, int | None, int | None]
- ],
- ) -> list[dict[str, Any]]:
- return [
+ f"poll_ballot/{id_}",
{
- "voted": date[0],
- "present": date[1],
- "user_id": date[2],
- "vote_delegated_to_user_id": date[3],
- **({"user_merged_into_id": date[4]} if date[4] else {}),
- **({"delegation_user_merged_into_id": date[5]} if date[5] else {}),
- }
- for date in voted_present_user_delegated_merged
- ]
-
- for poll_id, meeting_user_id in {2: 12, 5: 42, 6: 106}.items():
- self.assert_model_exists(
- f"poll/{poll_id}", {"voted_ids": [meeting_user_id]}
+ "value": str(id_ + 4),
+ "acting_meeting_user_id": None,
+ "represented_meeting_user_id": None,
+ },
)
+
self.assert_model_exists("poll/1", {"voted_ids": [12, 15]})
+ self.assert_model_exists("poll/2", {"voted_ids": [12]})
for id_ in [3, 4]:
self.assert_model_exists(f"poll/{id_}", {"voted_ids": None})
+ self.assert_model_exists("poll/5", {"voted_ids": [42]})
+ self.assert_model_exists("poll/6", {"voted_ids": [106 + add_to_creatable_ids]})
- # @pytest.mark.skip(reason="Finalize and unskip when new vote service is ready")
def test_merge_with_polls_correct(self) -> None:
password = self.assert_model_exists("user/2")["password"]
self.create_polls_with_correct_votes()
@@ -1186,22 +1074,53 @@ def test_merge_with_polls_correct(self) -> None:
self.assert_status_code(response, 200)
self.assert_merge_with_polls_correct(password)
- # @pytest.mark.skip(reason="Finalize and unskip when new vote service is ready")
def test_merge_with_polls_and_subsequent_merges(self) -> None:
password = self.assert_model_exists("user/2")["password"]
self.create_polls_with_correct_votes()
- e = self.datastore.get_everything()
response = self.request("user.merge_together", {"id": 3, "user_ids": [4]})
- e = self.datastore.get_everything()
self.assert_status_code(response, 200)
response = self.request("user.merge_together", {"id": 2, "user_ids": [3]})
- e = self.datastore.get_everything()
self.assert_status_code(response, 200)
self.assert_merge_with_polls_correct(password, 1)
- # @pytest.mark.skip(reason="Finalize and unskip when new vote service is ready")
def test_merge_with_polls_all_errors(self) -> None:
- self.create_polls_with_correct_votes(True)
+ self.set_up_polls_for_merge()
+ self.set_models(
+ {
+ "poll/4": {"state": Poll.STATE_STARTED},
+ "meeting_user/12": {"poll_voted_ids": [2]},
+ "meeting_user/14": {"poll_voted_ids": [1]},
+ "meeting_user/15": {"poll_voted_ids": [1]},
+ "meeting_user/43": {"poll_voted_ids": [5]},
+ "meeting_user/73": {"poll_voted_ids": [6]},
+ "poll_ballot/1": {
+ "value": "no",
+ "poll_id": 1,
+ "acting_meeting_user_id": 14,
+ "represented_meeting_user_id": 14,
+ },
+ "poll_ballot/2": {
+ "value": "no",
+ "poll_id": 1,
+ "acting_meeting_user_id": 14,
+ "represented_meeting_user_id": 15,
+ },
+ "poll_ballot/3": {
+ "value": "4",
+ "poll_id": 2,
+ "acting_meeting_user_id": 12,
+ "represented_meeting_user_id": 12,
+ },
+ "poll_ballot/4": {
+ "value": "abstain",
+ "poll_id": 5,
+ "acting_meeting_user_id": 43,
+ "represented_meeting_user_id": 43,
+ },
+ "poll_ballot/5": {"value": "9", "poll_id": 6},
+ "poll_ballot/6": {"value": "10", "poll_id": 6},
+ }
+ )
response = self.request("user.merge_together", {"id": 2, "user_ids": [3, 4, 5]})
self.assert_status_code(response, 400)
assert (
@@ -1212,8 +1131,7 @@ def test_merge_with_polls_all_errors(self) -> None:
"some of the selected users have different delegations roles in meeting(s) 1",
"some of the selected users are delegating votes to each other in meeting(s) 1",
"among the selected users multiple voted in poll(s) 1",
- "multiple of the selected users are among the options in poll(s) 1, 4",
- "multiple of the selected users are in the same candidate list in poll(s) 2, 3",
+ "multiple of the selected users are among the options in poll(s) 1, 2, 3, 4",
]
)
in response.json["message"]
@@ -2414,7 +2332,6 @@ def test_merge_archived_normal(self) -> None:
self.archive_all_meetings()
self.test_merge_normal()
- # @pytest.mark.skip(reason="Finalize and unskip when new vote service is ready")
def test_merge_archived_polls(self) -> None:
password = self.assert_model_exists("user/2")["password"]
self.create_polls_with_correct_votes()
From 6bba33690dce06466db97248ab91905d8a7842d3 Mon Sep 17 00:00:00 2001
From: Viktoriia Krasnovyd <114735598+vkrasnovyd@users.noreply.github.com>
Date: Mon, 8 Jun 2026 17:48:24 +0200
Subject: [PATCH 14/23] [vote] Complete export_helper, tests and docs (#3608)
---
docs/actions/meeting.create.md | 1 -
docs/actions/meeting.update.md | 3 -
docs/actions/poll.create.md | 82 -------
docs/actions/poll.update.md | 57 -----
docs/actions/user.merge_together.md | 2 +-
.../action/actions/vote/create.py | 77 -------
openslides_backend/models/checker.py | 4 +-
openslides_backend/services/vote/adapter.py | 40 +---
openslides_backend/services/vote/interface.py | 22 +-
openslides_backend/shared/export_helper.py | 77 ++-----
tests/database/reader/system/util.py | 1 -
.../system/action/agenda_item/test_forward.py | 22 +-
tests/system/action/meeting/test_archive.py | 2 +-
tests/system/action/meeting/test_clone.py | 73 +++---
tests/system/action/meeting/test_create.py | 9 +-
tests/system/action/meeting/test_delete.py | 16 +-
tests/system/action/meeting/test_import.py | 208 +++++++++++-------
tests/system/action/meeting/test_update.py | 8 -
.../system/action/meeting_user/test_update.py | 20 +-
.../test_delete_history_information.py | 48 ++--
tests/system/action/projector/test_toggle.py | 2 +-
tests/system/action/test_migration_route.py | 3 +
tests/system/action/user/test_delete.py | 39 +---
tests/system/base.py | 7 +-
.../system/migrations/test_0100_init_reldb.py | 3 +
tests/system/presenter/test_check_database.py | 15 +-
.../presenter/test_check_database_all.py | 15 +-
tests/system/presenter/test_export_meeting.py | 149 ++++++++++---
tests/system/util.py | 17 +-
29 files changed, 422 insertions(+), 600 deletions(-)
delete mode 100644 docs/actions/poll.create.md
delete mode 100644 docs/actions/poll.update.md
delete mode 100644 openslides_backend/action/actions/vote/create.py
diff --git a/docs/actions/meeting.create.md b/docs/actions/meeting.create.md
index 650cff4f90..ef701df30f 100644
--- a/docs/actions/meeting.create.md
+++ b/docs/actions/meeting.create.md
@@ -43,7 +43,6 @@ The field `is_active_in_organization_id` is set to the organization_id.
If a meeting is created,
* `motion_poll_default_type` is `pseudoanonymous`
-* `motion_poll_default_method` is `YNA`
* `assignment_poll_default_type` is `pseudoanonymous`
* `assignment_poll_default_method` is `Y`
* `assignment_poll_default_group_ids`, `motion_poll_default_group_ids` and `topic_poll_default_group_ids` have the Delegates group.
diff --git a/docs/actions/meeting.update.md b/docs/actions/meeting.update.md
index 062bc54950..2b88ab16ac 100644
--- a/docs/actions/meeting.update.md
+++ b/docs/actions/meeting.update.md
@@ -121,10 +121,8 @@
motion_poll_ballot_paper_selection: string;
motion_poll_ballot_paper_number: number;
motion_poll_default_type: string;
- motion_poll_default_method: string;
motion_poll_default_onehundred_percent_base: string;
motion_poll_default_group_ids: Id[];
- motion_poll_default_backend: string;
motion_poll_projection_name_order_first: string;
motion_poll_projection_max_columns: number;
@@ -158,7 +156,6 @@
assignment_poll_default_method: string;
assignment_poll_default_onehundred_percent_base: string;
assignment_poll_default_group_ids: Id[];
- assignment_poll_default_backend: string;
topic_poll_default_group_ids: Id[];
diff --git a/docs/actions/poll.create.md b/docs/actions/poll.create.md
deleted file mode 100644
index 5b7a729f3b..0000000000
--- a/docs/actions/poll.create.md
+++ /dev/null
@@ -1,82 +0,0 @@
-## Payload
-
-Helper Interface for options to create:
-```js
-Interface Option {
- // Exactly one of text, content_object_id or poll_candidate_user_ids must be given
- text: string, // topic-poll
- content_object_id: Fqid, // must be one of user or motion.
- poll_candidate_user_ids: [user_ids], // sorted list of user ids for candidate list election
-
- // Optionally and only for type==analog, votes can be given
- Y: decimal(6), // Y, YN, YNA mode
- N: decimal(6), // N, YN, YNA mode
- A: decimal(6) // YNA mode
-}
-```
-
-Payload:
-```js
-{
-// Required
- title: string,
- type: string,
- pollmethod: string,
-
- meeting_id: Id,
- options: Option[], // must have at least one entry.
-
-// Optional
- content_object_id: Fqid,
- description: string,
- min_votes_amount: number,
- max_votes_amount: number,
- max_votes_per_option: number,
- global_yes: boolean,
- global_no: boolean,
- global_abstain: boolean,
- onehundred_percent_base: string,
- backend: string,
-
-// Optional, only for type==named
- live_voting_enabled: boolean,
-
-// Only for non analog types
- entitled_group_ids: Id[],
-
-// Only for type==analog
- publish_immediately: boolean,
-
-// Optionally and only for type==analog, votes can be given
- votesvalid: decimal(6),
- votesinvalid: decimal(6),
- votescast: decimal(6),
- amount_global_yes: decimal(6),
- amount_global_no: decimal(6),
- amount_global_abstain: decimal(6)
-}
-```
-
-## Action
-If an analog poll with votes is given, the state is set to `finished` if at least one vote value is given. if `publish_immediately` is true and some vote value is given, the state is set to `published`. All options given are created as instances of the `option` model. If some options have values (for analog polls), `vote` objects have to be created, one for each option and vote value (`Y`, `N`, `A`).
-
-The options must be unique in the way that each non-empty `text` and non-empty `content_object_id` can only exist once. The `option/weight` is set in the order the options are given in the payload. A global option is created.
-
-If the `type` is `pseudoanonymous`, `is_pseudoanonymized` is set to `true`.
-
-If the `content_object_id` points to a `motion` and the `motion_state` of the motion misses `allow_create_poll`, it is forbidden to create a poll.
-
-The `entitled_group_ids` may not contain the meetings `anonymous_group_id`.
-
-The `max_votes_per_option` and `min_votes_amount` must be smaller or equal to `max_votes_amount`.
-
-The `live_voting_enabled` could be set for named motion and following named assignment polls:
-- polls with pollmethod `Y` and not `global_yes` and `max_votes_amount` of 1
-- polls with pollmethod `YNA` or `YN` and only one option
-- list polls
-
-## Permissions
-The request user needs:
-- `motion.can_manage_polls` if the poll's content object is a motion
-- `assignment.can_manage_polls` if the poll's content object is an assignment
-- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/poll.update.md b/docs/actions/poll.update.md
deleted file mode 100644
index 5c4953d4b5..0000000000
--- a/docs/actions/poll.update.md
+++ /dev/null
@@ -1,57 +0,0 @@
-## Payload
-```js
-{
-// Required
- id: Id,
-
-// Optional, only if state == created
- pollmethod: string,
- min_votes_amount: number,
- max_votes_amount: number,
- max_votes_per_option: number,
- global_yes: boolean,
- global_no: boolean,
- global_abstain: boolean,
- backend: string,
-
-// Optional, only if state == created, only for non analog types
- entitled_group_ids: Id[],
-
-// Optional, every state
- title: string,
- description: string,
- onehundred_percent_base: string,
-
-// Optional, type==analog, every state
- votesvalid: number,
- votesinvalid: number,
- votescast: number,
- publish_immediately: boolean,
-
-// Optional, type==named
- live_voting_enabled: boolean
-
-// action called internally
- entitled_users_at_stop: json
-}
-```
-
-## Action
-For analog polls: If the state is created and at least one vote value is given (`votesvalid`/`votesinvalid`/`votescast`), the state is set to finished. If additionally `publish_immediately` is given, the state is set to published.
-
-For electronic polls some fields can only be updated, if the state is *created*.
-
-The `entitled_group_ids` may not contain the meetings `anonymous_group_id`.
-
-The `max_votes_per_option` and `min_votes_amount` must be smaller or equal to `max_votes_amount` after the model has been updated.
-
-The `live_voting_enabled` could be set for named motion and following named assignment polls:
-- polls with pollmethod `Y` and not `global_yes` and `max_votes_amount` of 1
-- polls with pollmethod `YNA` or `YN` and only one option
-- list polls
-
-## Permissions
-The request user needs:
-- `motion.can_manage_polls` if the poll's content object is a motion
-- `assignment.can_manage_polls` if the poll's content object is an assignment
-- `poll.can_manage` if the poll's content object is a topic
diff --git a/docs/actions/user.merge_together.md b/docs/actions/user.merge_together.md
index 0f052932e0..5846030064 100644
--- a/docs/actions/user.merge_together.md
+++ b/docs/actions/user.merge_together.md
@@ -44,7 +44,7 @@ An error is thrown if:
- There are conflicts regarding polls, i.e. two or more of the selected users...
- Any affected meeting_users have groups that are currently entitled to work on any poll
- Any affected meeting_users _who share a meeting_:
- - are meeting_users of the poll_config_option on the same poll
+ - are meeting_users of the poll_option on the same poll
- have voted on the same poll (delegated or not)
- have running speakers
- are in a meeting without `list_of_speakers_allow_multiple_speakers` and have waiting speakers on the same list who cannot be merged together into at most one point_of_order and one normal speech. Speeches may not be merged if there are multiple different values (empty does count) within any of the fields:
diff --git a/openslides_backend/action/actions/vote/create.py b/openslides_backend/action/actions/vote/create.py
deleted file mode 100644
index a02bc677fa..0000000000
--- a/openslides_backend/action/actions/vote/create.py
+++ /dev/null
@@ -1,77 +0,0 @@
-from typing import cast
-
-from openslides_backend.action.util.typing import ActionData
-from openslides_backend.services.database.commands import GetManyRequest
-
-from ....models.models import Vote
-from ...mixins.create_action_with_inferred_meeting import (
- CreateActionWithInferredMeeting,
-)
-from ...util.action_type import ActionType
-from ...util.default_schema import DefaultSchema
-from ...util.register import register_action
-
-
-@register_action("vote.create", action_type=ActionType.BACKEND_INTERNAL)
-class VoteCreate(CreateActionWithInferredMeeting):
- """
- Internal action to create a vote.
- """
-
- model = Vote()
- schema = DefaultSchema(Vote()).get_create_schema(
- required_properties=[
- "weight",
- "value",
- "option_id",
- "user_token",
- ],
- optional_properties=["delegated_user_id", "user_id"],
- )
-
- relation_field_for_meeting = "option_id"
-
- def prefetch(self, action_data: ActionData) -> None:
- self.datastore.get_many(
- [
- GetManyRequest(
- "option",
- list({instance["option_id"] for instance in action_data}),
- ["meeting_id", "vote_ids"],
- ),
- ],
- use_changed_models=False,
- )
- meeting_users = self.datastore.get_many(
- [
- GetManyRequest(
- "meeting_user",
- list(
- {
- cast(int, instance.get(fname))
- for instance in action_data
- for fname in (
- "meeting_user_id",
- "delegated_meeting_user_id",
- )
- if instance.get(fname)
- }
- ),
- ["id", "user_id", "vote_ids", "delegated_vote_ids"],
- ),
- ],
- use_changed_models=False,
- lock_result=False,
- )["meeting_user"]
-
- self.datastore.get_many(
- [
- GetManyRequest(
- "user",
- list({mu["user_id"] for mu in meeting_users.values()}),
- ["id", "poll_voted_ids"],
- ),
- ],
- use_changed_models=False,
- lock_result=False,
- )
diff --git a/openslides_backend/models/checker.py b/openslides_backend/models/checker.py
index ac0abdfdde..ecc1709225 100644
--- a/openslides_backend/models/checker.py
+++ b/openslides_backend/models/checker.py
@@ -192,12 +192,12 @@ def check_timestamp(value: Any) -> bool:
"meeting",
"user",
"mediafile",
- "vote",
+ "poll_ballot",
"poll_config_approval",
"poll_config_selection",
"poll_config_rating_score",
"poll_config_rating_approval",
- "poll_config_option",
+ "poll_option",
}
diff --git a/openslides_backend/services/vote/adapter.py b/openslides_backend/services/vote/adapter.py
index ddbadc99c5..7868ca596e 100644
--- a/openslides_backend/services/vote/adapter.py
+++ b/openslides_backend/services/vote/adapter.py
@@ -1,5 +1,5 @@
from enum import StrEnum
-from typing import Any, Literal
+from typing import Any
import requests
import simplejson as json
@@ -79,43 +79,13 @@ def make_request(
)
raise VoteServiceException(f"Cannot reach the vote service on {endpoint}.")
- def create(self, payload: dict[str, Any]) -> dict[str, Any]:
- endpoint = self.get_endpoint()
- return self.retrieve(endpoint, payload=payload)
-
- def update(self, id: int, payload: dict[str, Any]) -> dict[str, Any]:
- endpoint = self.get_endpoint(id)
- return self.retrieve(endpoint, payload=payload)
-
- def delete(self, id: int) -> dict[str, Any]:
- endpoint = self.get_endpoint(id)
- return self.retrieve(endpoint, RequestMethod.DELETE)
-
- def start(self, id: int) -> dict[str, Any]:
- endpoint = self.get_endpoint(id, "start")
- return self.retrieve(endpoint)
-
- def finalize(
- self,
- id: int,
- optional_attributes: list[Literal["publish", "anonymize"]] = [],
- ) -> dict[str, Any]:
- endpoint = self.get_endpoint(id, "finalize")
- if optional_attributes:
- endpoint += f"?{'&'.join(optional_attributes)}"
- return self.retrieve(endpoint)
-
- def reset(self, id: int) -> dict[str, Any]:
- endpoint = self.get_endpoint(id, "reset")
- return self.retrieve(endpoint)
-
- def vote(self, id: int, payload: dict[str, Any]) -> dict[str, Any]:
- endpoint = self.get_endpoint(id, "vote")
- return self.retrieve(endpoint, payload=payload)
-
def get_endpoint(self, id: int | None = None, route: str | None = None) -> str:
return (
f"{self.url}/poll"
+ (f"/{id}" if id else "")
+ (f"/{route}" if route else "")
)
+
+ def delete(self, id: int) -> dict[str, Any]:
+ endpoint = self.get_endpoint(id)
+ return self.retrieve(endpoint, RequestMethod.DELETE)
diff --git a/openslides_backend/services/vote/interface.py b/openslides_backend/services/vote/interface.py
index ef69f58ad5..7cc2844b30 100644
--- a/openslides_backend/services/vote/interface.py
+++ b/openslides_backend/services/vote/interface.py
@@ -1,5 +1,5 @@
from abc import abstractmethod
-from typing import Any, Literal, Protocol
+from typing import Any, Protocol
from ..shared.authenticated_service import AuthenticatedServiceInterface
@@ -9,25 +9,5 @@ class VoteService(AuthenticatedServiceInterface, Protocol):
Interface of the vote service.
"""
- @abstractmethod
- def create(self, payload: dict[str, Any]) -> dict[str, Any]: ...
-
- @abstractmethod
- def update(self, id: int, payload: dict[str, Any]) -> dict[str, Any]: ...
-
@abstractmethod
def delete(self, id: int) -> dict[str, Any]: ...
-
- @abstractmethod
- def start(self, id: int) -> dict[str, Any]: ...
-
- @abstractmethod
- def finalize(
- self, id: int, optional_attributes: list[Literal["publish", "anonymize"]] = []
- ) -> dict[str, Any]: ...
-
- @abstractmethod
- def reset(self, id: int) -> dict[str, Any]: ...
-
- @abstractmethod
- def vote(self, id: int, payload: dict[str, Any]) -> dict[str, Any]: ...
diff --git a/openslides_backend/shared/export_helper.py b/openslides_backend/shared/export_helper.py
index ada4b93633..d24f97f2a7 100644
--- a/openslides_backend/shared/export_helper.py
+++ b/openslides_backend/shared/export_helper.py
@@ -15,7 +15,7 @@
RelationField,
RelationListField,
)
-from ..models.models import Meeting, PollBallot
+from ..models.models import Meeting
from ..services.database.commands import GetManyRequest
from ..services.database.interface import Database
from .patterns import collection_from_fqid, fqid_from_collection_and_id, id_from_fqid
@@ -208,62 +208,29 @@ def export_meeting(
user_ids.add(results["meeting_user"][id_]["user_id"])
add_users(list(user_ids), export, meeting_id, datastore, internal_target)
- # Get related ballots
+ # Get related configs, options and ballots
if polls := export.get("poll"):
- ballot_ids = [
- ballot_id
- for poll in polls.values()
- for ballot_id in poll.get("ballot_ids", [])
- ]
- if ballot_ids:
- ballots = datastore.get_many(
- [
- GetManyRequest(
- "poll_ballot", ballot_ids, get_fields_for_export("poll_ballot")
- )
- ],
- lock_result=False,
- use_changed_models=False,
- )["poll_ballot"]
- for ballot_id, instance in ballots.items():
- export.setdefault("poll_ballot", {})[str(ballot_id)] = instance
- for field_name, value in instance.items():
- model_field = PollBallot().get_field(field_name)
- if (
- not isinstance(model_field, RelationField)
- or model_field.get_own_field_name() == "poll_id"
- ):
- continue
- collection, relation_field = next(iter(model_field.to.items()))
- export[collection][str(value)].setdefault(
- relation_field, []
- ).append(ballot_id)
-
- config_ids = [poll.get("config_id") for poll in polls.values()]
-
- for config_fqid in config_ids:
- collection, id_ = config_fqid.split("/")
- instance = datastore.get(
- config_fqid,
- list(get_fields_for_export(collection)),
- lock_result=False,
- use_changed_models=False,
+ related_collections: dict[str, list[int]] = {}
+ for poll in polls.values():
+ for collection_base_name in ["ballot", "option"]:
+ if ids := poll.get(f"{collection_base_name}_ids"):
+ related_collections.setdefault(
+ f"poll_{collection_base_name}", list()
+ ).extend(ids)
+ config_collection, config_id = poll["config_id"].split("/")
+ related_collections.setdefault(config_collection, list()).append(
+ int(config_id)
+ )
+ related_models = datastore.get_many(
+ [
+ GetManyRequest(collection, ids, get_fields_for_export(collection))
+ for collection, ids in related_collections.items()
+ ]
+ )
+ for collection, data in related_models.items():
+ export.setdefault(collection, {}).update(
+ {str(id_): instance_data for id_, instance_data in data.items()}
)
- export.setdefault(collection, {})[str(id_)] = instance
- if option_ids := instance.get("option_ids"):
- options = datastore.get_many(
- [
- GetManyRequest(
- "poll_config_option",
- option_ids,
- get_fields_for_export("poll_config_option"),
- )
- ],
- lock_result=False,
- use_changed_models=False,
- )["poll_config_option"]
- for id_, option in options.items():
- export.setdefault("poll_config_option", {})[str(id_)] = option
# Sort instances by id within each collection
for collection, instances in export.items():
diff --git a/tests/database/reader/system/util.py b/tests/database/reader/system/util.py
index 9e153c4bab..6088b202f0 100644
--- a/tests/database/reader/system/util.py
+++ b/tests/database/reader/system/util.py
@@ -233,7 +233,6 @@
"meeting.can_see_history",
"motion.can_manage",
"motion.can_manage_polls",
- "poll.can_manage",
"projector.can_manage",
"tag.can_manage",
"user.can_manage",
diff --git a/tests/system/action/agenda_item/test_forward.py b/tests/system/action/agenda_item/test_forward.py
index 6142140b1c..c5a893ef8d 100644
--- a/tests/system/action/agenda_item/test_forward.py
+++ b/tests/system/action/agenda_item/test_forward.py
@@ -3293,22 +3293,22 @@ def test_forward_structure_level_transfer_only_via_meeting_users(self) -> None:
"meeting_id": 1,
"title": "Will not transfer",
"visibility": Poll.VISIBILITY_SECRET,
- "config_id": "poll_config_rating_approval/34",
+ "config_id": "poll_config_rating_score/34",
"state": Poll.STATE_CREATED,
- "min_votes_amount": 1,
- "max_votes_amount": 1,
- "max_votes_per_option": 1,
- "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_Y,
"content_object_id": "topic/11",
},
- "poll_config_approval/34": {"poll_id": 1, "allow_abstain": False},
- "poll_config_option/123": {
- "meeting_id": 1,
+ "poll_config_rating_score/34": {
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID,
+ "min_options_amount": 1,
+ "max_options_amount": 1,
+ "max_votes_per_option": 1,
+ },
+ "poll_option/123": {
"poll_id": 1234,
"text": "Option A",
},
- "poll_config_option/234": {
- "poll_config_id": "poll_config_approval/34",
+ "poll_option/234": {
+ "poll_id": 1234,
"text": "Option B",
},
# mediafiles
@@ -3488,7 +3488,7 @@ def test_forward_structure_level_transfer_only_via_meeting_users(self) -> None:
"meeting_mediafile": 36,
"poll": 1235,
"poll_config_approval": 35,
- "poll_config_option": 235,
+ "poll_option": 235,
"speaker": 38,
"structure_level_list_of_speakers": 15,
"point_of_order_category": 8,
diff --git a/tests/system/action/meeting/test_archive.py b/tests/system/action/meeting/test_archive.py
index 31939399c2..43e2f3b133 100644
--- a/tests/system/action/meeting/test_archive.py
+++ b/tests/system/action/meeting/test_archive.py
@@ -115,7 +115,7 @@ def create_poll(self, base: int, state: str, published: bool = False) -> None:
"published": published,
},
f"poll_config_approval/{base}": {
- "poll_id": base,
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID,
},
}
)
diff --git a/tests/system/action/meeting/test_clone.py b/tests/system/action/meeting/test_clone.py
index 9a7a18c458..9680445b7a 100644
--- a/tests/system/action/meeting/test_clone.py
+++ b/tests/system/action/meeting/test_clone.py
@@ -477,10 +477,8 @@ def test_clone_with_change_recommendation_complex(self) -> None:
"motion_poll_ballot_paper_selection": "CUSTOM_NUMBER",
"motion_poll_ballot_paper_number": 8,
"motion_poll_default_type": "pseudoanonymous",
- "motion_poll_default_method": "YNA",
- "motion_poll_default_onehundred_percent_base": "YNA",
+ "motion_poll_default_onehundred_percent_base": "valid",
"motion_poll_default_group_ids": [],
- "motion_poll_default_backend": "fast",
},
)
self.set_user_groups(1, [5])
@@ -1385,8 +1383,7 @@ def test_clone_with_settings(self) -> None:
"motion_poll_ballot_paper_selection": "NUMBER_OF_DELEGATES",
"motion_poll_ballot_paper_number": 42,
"motion_poll_default_type": "pseudoanonymous",
- "motion_poll_default_method": "YNA",
- "motion_poll_default_onehundred_percent_base": "YN",
+ "motion_poll_default_onehundred_percent_base": "yes_no",
"users_enable_presence_view": True,
"users_enable_vote_weight": True,
"users_enable_vote_delegations": True,
@@ -1409,7 +1406,7 @@ def test_clone_with_settings(self) -> None:
"assignment_poll_sort_poll_result_by_votes": True,
"assignment_poll_default_type": "pseudoanonymous",
"assignment_poll_default_method": "YNA",
- "assignment_poll_default_onehundred_percent_base": "YNA",
+ "assignment_poll_default_onehundred_percent_base": "valid",
}
self.meeting_data.update(settings)
self.set_test_data_with_admin()
@@ -1814,24 +1811,25 @@ def test_clone_vote_delegation(self) -> None:
def test_clone_vote_delegated_vote(self) -> None:
self.set_test_data()
self.create_meeting(4)
- self.create_motion(1, 1)
- self.create_motion(4, 4)
self.set_user_groups(1, [2, 5])
- for poll_id in range(1, 3):
+ for poll_id in [1, 4]:
+ self.create_motion(poll_id, poll_id)
self.set_models(
{
f"poll/{poll_id}": {
"title": f"Poll {poll_id}",
- "meeting_id": 1 if poll_id == 1 else 4,
+ "meeting_id": poll_id,
"content_object_id": f"motion/{poll_id}",
"visibility": Poll.VISIBILITY_NAMED,
"config_id": f"poll_config_rating_approval/{poll_id}",
"state": Poll.STATE_STARTED,
},
- f"poll_config_rating_approval/{poll_id}": {"poll_id": poll_id},
- f"ballot/{poll_id}": {
- "acting_meeting_user_id": poll_id,
- "represented_meeting_user_id": poll_id,
+ f"poll_config_rating_approval/{poll_id}": {
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID
+ },
+ f"poll_ballot/{poll_id}": {
+ "acting_meeting_user_id": 1 if poll_id == 1 else 2,
+ "represented_meeting_user_id": 1 if poll_id == 1 else 2,
"poll_id": poll_id,
},
},
@@ -1839,23 +1837,26 @@ def test_clone_vote_delegated_vote(self) -> None:
response = self.request("meeting.clone", {"meeting_id": 1})
self.assert_status_code(response, 200)
self.assert_model_exists(
- "ballot/3",
+ "poll_ballot/5",
{
- "acting_meeting_user_id": 1,
- "represented_meeting_user_id": 1,
- "poll_id": 3,
+ "acting_meeting_user_id": 3,
+ "represented_meeting_user_id": 3,
+ "poll_id": 5,
},
)
self.assert_model_exists(
"user/1",
+ {"meeting_user_ids": [1, 2, 3], "meeting_ids": [1, 4, 5]},
+ )
+ self.assert_model_exists(
+ "meeting_user/3",
{
- "meeting_user_ids": [1, 2, 3],
- # "acting_vote_ids": [1, 2, 3, 4],
- # "represented_vote_ids": [1, 2, 3, 4],
- "meeting_ids": [1, 4, 5],
+ "user_id": 1,
+ "meeting_id": 5,
+ "acting_ballot_ids": [5],
+ "represented_ballot_ids": [5],
},
)
- self.assert_model_exists("meeting_user/3", {"user_id": 1, "meeting_id": 5})
def test_with_action_worker(self) -> None:
"""action_worker shouldn't be cloned"""
@@ -2077,20 +2078,20 @@ def test_clone_with_list_election(self) -> None:
"config_id": "poll_config_approval/1",
"state": Poll.STATE_CREATED,
},
- "poll_config_approval/1": {"id": 1, "poll_id": 1},
- }
- )
- for id_ in range(1, 4):
- self.set_models(
- {
- f"poll_config_option/{id_}": {
+ "poll_config_approval/1": {
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID
+ },
+ **{
+ f"poll_option/{id_}": {
"id": id_,
"weight": id_,
- "poll_config_id": "poll_config_approval/1",
"meeting_user_id": id_ + 1,
+ "poll_id": 1,
}
- }
- )
+ for id_ in range(1, 4)
+ },
+ }
+ )
response = self.request("meeting.clone", {"meeting_id": 1})
self.assert_status_code(response, 200)
@@ -2120,14 +2121,14 @@ def test_clone_with_list_election(self) -> None:
)
self.assert_model_exists(
"poll_config_approval/2",
- {"poll_id": 2, "option_ids": [4, 5, 6]},
+ {"onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID},
)
for id_ in range(4, 7):
self.assert_model_exists(
- f"poll_config_option/{id_}",
+ f"poll_option/{id_}",
{
"weight": id_ - 3,
- "poll_config_id": "poll_config_approval/2",
+ "poll_id": 2,
"meeting_user_id": id_ + 2,
},
)
diff --git a/tests/system/action/meeting/test_create.py b/tests/system/action/meeting/test_create.py
index 2817620a28..8399fe120d 100644
--- a/tests/system/action/meeting/test_create.py
+++ b/tests/system/action/meeting/test_create.py
@@ -4,7 +4,7 @@
from openslides_backend.i18n.translator import Translator
from openslides_backend.i18n.translator import translate as _
-from openslides_backend.models.models import Meeting
+from openslides_backend.models.models import Meeting, Poll
from openslides_backend.permissions.management_levels import OrganizationManagementLevel
from openslides_backend.shared.util import ONE_ORGANIZATION_FQID
from tests.system.action.base import BaseActionTestCase
@@ -206,10 +206,9 @@ def test_check_action_data_fields(self) -> None:
"start_time": datetime.fromtimestamp(1608120653, ZoneInfo("UTC")),
"end_time": datetime.fromtimestamp(1608121653, ZoneInfo("UTC")),
"external_id": external_id,
- "assignment_poll_default_type": "pseudoanonymous",
- "assignment_poll_default_method": "Y",
- "motion_poll_default_type": "pseudoanonymous",
- "motion_poll_default_method": "YNA",
+ "assignment_poll_default_type": Poll.VISIBILITY_SECRET,
+ "assignment_poll_default_method": Poll.METHOD_SELECTION,
+ "motion_poll_default_type": Poll.VISIBILITY_SECRET,
},
)
diff --git a/tests/system/action/meeting/test_delete.py b/tests/system/action/meeting/test_delete.py
index 9aa2c0b030..1f4ea2ae4c 100644
--- a/tests/system/action/meeting/test_delete.py
+++ b/tests/system/action/meeting/test_delete.py
@@ -252,13 +252,15 @@ def test_delete_with_poll_candidates_and_speakers(self) -> None:
"visibility": Poll.VISIBILITY_MANUALLY,
"meeting_id": 1,
},
- "poll_config_approval/160": {"poll_id": 150},
- "poll_config_option/180": {
- "poll_config_id": "poll_config_approval/160",
+ "poll_config_approval/160": {
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID
+ },
+ "poll_option/180": {
+ "poll_id": 150,
"meeting_user_id": 1,
},
- "poll_config_option/181": {
- "poll_config_id": "poll_config_approval/160",
+ "poll_option/181": {
+ "poll_id": 150,
"meeting_user_id": 2,
},
"list_of_speakers/190": {
@@ -298,8 +300,8 @@ def test_delete_with_poll_candidates_and_speakers(self) -> None:
"assignment/140",
"poll/150",
"poll_config_approval/160",
- "poll_config_option/180",
- "poll_config_option/181",
+ "poll_option/180",
+ "poll_option/181",
"list_of_speakers/190",
"speaker/210",
"speaker/211",
diff --git a/tests/system/action/meeting/test_import.py b/tests/system/action/meeting/test_import.py
index 53a7dc6004..bd79164c9b 100644
--- a/tests/system/action/meeting/test_import.py
+++ b/tests/system/action/meeting/test_import.py
@@ -140,10 +140,8 @@ def create_request_data(
"motion_poll_ballot_paper_selection": "CUSTOM_NUMBER",
"motion_poll_ballot_paper_number": 8,
"motion_poll_default_type": "pseudoanonymous",
- "motion_poll_default_method": "YNA",
- "motion_poll_default_onehundred_percent_base": "YNA",
+ "motion_poll_default_onehundred_percent_base": "valid",
"motion_poll_default_group_ids": [],
- "motion_poll_default_backend": "fast",
"users_enable_presence_view": True,
"users_enable_vote_weight": True,
"users_enable_vote_delegations": True,
@@ -168,7 +166,6 @@ def create_request_data(
"assignment_poll_default_method": "votes",
"assignment_poll_default_onehundred_percent_base": "valid",
"assignment_poll_default_group_ids": [],
- "assignment_poll_default_backend": "fast",
"poll_ballot_paper_selection": "CUSTOM_NUMBER",
"poll_ballot_paper_number": 8,
"poll_sort_poll_result_by_votes": True,
@@ -320,7 +317,11 @@ def create_request_data(
if collection not in data["meeting"]:
data["meeting"][collection] = models
else:
- data["meeting"][collection].update(models)
+ for id_, model in models.items():
+ if id_ in data["meeting"][collection]:
+ data["meeting"][collection][id_].update(model)
+ else:
+ data["meeting"][collection][id_] = model
return data
@@ -432,6 +433,44 @@ def get_meeting_mediafile_data(
**data,
}
+ def get_assignment_poll_data(self, obj_id: int) -> dict[str, Any]:
+ return {
+ "poll": {
+ str(obj_id): {
+ "id": obj_id,
+ "title": "pull",
+ "config_id": f"poll_config_rating_approval/{obj_id + 1}",
+ "visibility": Poll.VISIBILITY_MANUALLY,
+ "state": Poll.STATE_STARTED,
+ "meeting_id": 1,
+ "content_object_id": f"assignment/{obj_id + 2}",
+ }
+ },
+ "poll_config_rating_approval": {
+ str(obj_id + 1): {
+ "id": obj_id + 1,
+ "poll_id": obj_id,
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID,
+ }
+ },
+ "assignment": {
+ str(obj_id + 2): {
+ "id": obj_id + 2,
+ "title": "just do it",
+ "meeting_id": 1,
+ "list_of_speakers_id": obj_id + 3,
+ "poll_ids": [obj_id],
+ }
+ },
+ "list_of_speakers": {
+ str(obj_id + 3): {
+ "id": obj_id + 3,
+ "content_object_id": f"assignment/{obj_id + 2}",
+ "meeting_id": 1,
+ }
+ },
+ }
+
def replace_migrated_projector_fields(self, data: dict[str, Any]) -> None:
data["meeting"]["meeting"]["1"][
"default_projector_current_list_of_speakers_ids"
@@ -2315,8 +2354,51 @@ def test_import_with_wrong_decimal(self) -> None:
in response.json["message"]
)
- def test_import_new_user_with_vote(self) -> None:
+ def prepare_user_with_ballot_data(self) -> dict[str, dict[str, Any]]:
+ data = self.create_request_data(
+ {
+ **self.get_assignment_poll_data(3),
+ "poll_ballot": {
+ "7": {
+ "id": 7,
+ "poll_id": 3,
+ "acting_meeting_user_id": 11,
+ "represented_meeting_user_id": 11,
+ },
+ },
+ "meeting": {
+ "1": {
+ "poll_ids": [3],
+ "assignment_ids": [5],
+ "list_of_speakers_ids": [6],
+ }
+ },
+ "group": {
+ "2": {
+ "poll_ids": [3],
+ }
+ },
+ "meeting_user": {
+ "11": {
+ "poll_voted_ids": [3],
+ "acting_ballot_ids": [7],
+ "represented_ballot_ids": [7],
+ }
+ },
+ }
+ )
+ data["meeting"]["poll"]["3"].update(
+ {
+ "entitled_group_ids": [2],
+ "ballot_ids": [7],
+ "voted_ids": [11],
+ }
+ )
+ return data
+
+ def test_import_new_user_with_ballot(self) -> None:
self.set_user_groups(1, [1])
+ self.create_motion(1, 30)
self.set_models(
{
"poll/1": {
@@ -2325,62 +2407,79 @@ def test_import_new_user_with_vote(self) -> None:
"visibility": Poll.VISIBILITY_MANUALLY,
"state": Poll.STATE_STARTED,
"meeting_id": 1,
- "content_object_id": "meeting/1",
+ "content_object_id": "motion/30",
},
- "poll_config_approval/1": {"poll_id": 1},
- "ballot/1": {
+ "poll_config_approval/1": {
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID,
+ },
+ "poll_ballot/1": {
"acting_meeting_user_id": 1,
"represented_meeting_user_id": 1,
"poll_id": 1,
},
+ "meeting_user/1": {"poll_voted_ids": [1]},
}
)
- data = self.create_request_data(
- {
- "poll_ballot": {
- "1": {
- "id": 1,
- "acting_meeting_user_id": 1,
- "represented_meeting_user_id": 1,
- "poll_id": 1,
- },
- },
- }
- )
- data["meeting"]["meeting_user"]["1"]["acting_ballot_ids"] = [1]
- data["meeting"]["meeting_user"]["1"]["represented_ballot_ids"] = [1]
+
+ data = self.prepare_user_with_ballot_data()
response = self.request("meeting.import", data)
self.assert_status_code(response, 200)
self.assert_model_exists(
"user/1",
{
"username": "admin",
- "meeting_user_ids": [2],
+ "meeting_user_ids": [1, 3],
},
)
self.assert_model_exists(
"user/2",
{
"username": "test",
- "meeting_user_ids": [1],
+ "meeting_user_ids": [2],
},
)
self.assert_model_exists(
"meeting_user/1",
{
- "user_id": 2,
+ "user_id": 1,
+ "meeting_id": 1,
"acting_ballot_ids": [1],
"represented_ballot_ids": [1],
+ "poll_voted_ids": [1],
},
)
self.assert_model_exists(
"meeting_user/2",
{
- "user_id": 1,
+ "user_id": 2,
+ "meeting_id": 2,
"acting_ballot_ids": [2],
"represented_ballot_ids": [2],
+ "poll_voted_ids": [2],
},
)
+ self.assert_model_exists(
+ "meeting_user/3",
+ {
+ "user_id": 1,
+ "meeting_id": 2,
+ "acting_ballot_ids": None,
+ "represented_ballot_ids": None,
+ "poll_voted_ids": None,
+ },
+ )
+ self.assert_model_exists(
+ "poll/2",
+ {
+ "config_id": "poll_config_rating_approval/1",
+ "content_object_id": "assignment/1",
+ "meeting_id": 2,
+ "ballot_ids": [2],
+ "voted_ids": [2],
+ "entitled_group_ids": [5],
+ },
+ )
+ self.assert_model_exists("poll_config_rating_approval/1", {"poll_id": 2})
def test_gender_import(self) -> None:
"""
@@ -2519,57 +2618,19 @@ def test_gender_import(self) -> None:
{"user_ids": [1, 2, 3, 4, 5, 6, 7], "gender_ids": [1, 4, 5, 6]},
)
- def test_import_existing_user_with_vote(self) -> None:
- self.set_user_groups(1, [1])
- self.set_models(
- {
- "poll/1": {
- "title": "pull",
- "config_id": "poll_config_approval/1",
- "visibility": Poll.VISIBILITY_MANUALLY,
- "state": Poll.STATE_STARTED,
- "meeting_id": 1,
- "content_object_id": "meeting/1",
- },
- "poll_config_approval/1": {"poll_id": 1},
- "vote/1": {
- "acting_meeting_user_id": 1,
- "represented_meeting_user_id": 1,
- "poll_id": 1,
- },
- }
- )
- data = self.create_request_data(
- {
- "poll_ballot": {
- "1": {
- "id": 1,
- "acting_meeting_user_id": 1,
- "represented_meeting_user_id": 1,
- },
- },
- }
- )
+ def test_import_existing_user_with_ballot(self) -> None:
+ data = self.prepare_user_with_ballot_data()
data["meeting"]["user"]["1"]["username"] = "admin"
data["meeting"]["user"]["1"]["last_name"] = ""
- data["meeting"]["meeting_user"]["1"]["acting_ballot_ids"] = [1]
- data["meeting"]["meeting_user"]["1"]["represented_ballot_ids"] = [1]
response = self.request("meeting.import", data)
self.assert_status_code(response, 200)
self.assert_model_exists(
"user/1",
- {
- "username": "admin",
- "meeting_user_ids": [1],
- },
+ {"username": "admin", "meeting_user_ids": [1]},
)
self.assert_model_exists(
- "meeting_user_ids/1",
- {
- "user_id": 1,
- "acting_ballot_ids": [1, 2],
- "represented_ballot_ids": [1, 2],
- },
+ "meeting_user/1",
+ {"user_id": 1, "acting_ballot_ids": [1], "represented_ballot_ids": [1]},
)
self.assert_model_not_exists("user/2")
@@ -2716,9 +2777,7 @@ def test_import_export_with_orga_mediafiles(self) -> None:
"motion_poll_ballot_paper_selection": "CUSTOM_NUMBER",
"motion_poll_ballot_paper_number": 8,
"motion_poll_default_type": "pseudoanonymous",
- "motion_poll_default_method": "YNA",
- "motion_poll_default_onehundred_percent_base": "YNA",
- "motion_poll_default_backend": "fast",
+ "motion_poll_default_onehundred_percent_base": "valid",
"motion_poll_projection_name_order_first": "last_name",
"motion_poll_projection_max_columns": 6,
"users_enable_presence_view": False,
@@ -2739,9 +2798,8 @@ def test_import_export_with_orga_mediafiles(self) -> None:
"assignment_poll_default_type": "pseudoanonymous",
"assignment_poll_default_method": "Y",
"assignment_poll_default_onehundred_percent_base": "valid",
- "assignment_poll_default_backend": "fast",
"poll_default_type": "analog",
- "poll_default_onehundred_percent_base": "YNA",
+ "poll_default_onehundred_percent_base": "valid",
"poll_default_live_voting_enabled": False,
"poll_couple_countdown": True,
**{field: [1] for field in Meeting.all_default_projectors()},
diff --git a/tests/system/action/meeting/test_update.py b/tests/system/action/meeting/test_update.py
index 924227642e..17d09aaa7c 100644
--- a/tests/system/action/meeting/test_update.py
+++ b/tests/system/action/meeting/test_update.py
@@ -288,14 +288,6 @@ def test_update_applause(self) -> None:
self.basic_test(data)
self.assert_model_exists("meeting/1", data)
- def test_update_poll_default_backend_fields(self) -> None:
- data = {
- "motion_poll_default_backend": "long",
- "assignment_poll_default_backend": "long",
- }
- self.basic_test(data)
- self.assert_model_exists("meeting/1", data)
-
def test_update_poll_default_live_voting_enabled(self) -> None:
self.basic_test({"poll_default_live_voting_enabled": True})
self.assert_model_exists(
diff --git a/tests/system/action/meeting_user/test_update.py b/tests/system/action/meeting_user/test_update.py
index 4b5717b8a4..91d6d31adc 100644
--- a/tests/system/action/meeting_user/test_update.py
+++ b/tests/system/action/meeting_user/test_update.py
@@ -108,7 +108,7 @@ def test_update_merge_fields_correct(self) -> None:
},
)
- def test_update_with_vote_related_fields(self) -> None:
+ def test_update_with_poll_related_fields(self) -> None:
self.create_meeting()
self.set_user_groups(1, [1])
self.create_user("dummy2", [1])
@@ -142,17 +142,19 @@ def test_update_with_vote_related_fields(self) -> None:
"sequential_number": 1,
"list_of_speakers_id": 1,
},
- "poll_config_option/1": {
- "poll_config_id": "poll_config_approval/1",
+ "poll_option/1": {
+ "poll_id": 1,
"meeting_user_id": 2,
},
- "poll_config_approval/1": {"poll_id": 1},
- "ballot/1": {
+ "poll_config_approval/1": {
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID
+ },
+ "poll_ballot/1": {
"poll_id": 1,
"acting_meeting_user_id": 1,
"represented_meeting_user_id": 1,
},
- "ballot/2": {
+ "poll_ballot/2": {
"poll_id": 1,
"acting_meeting_user_id": 1,
"represented_meeting_user_id": 2,
@@ -179,9 +181,9 @@ def test_update_with_vote_related_fields(self) -> None:
"represented_ballot_ids": [2],
},
"poll/1": {"voted_ids": [3]},
- "poll_config_option/1": {"meeting_user_id": 3},
- "ballot/1": {"acting_meeting_user_id": 3},
- "ballot/2": {"represented_meeting_user_id": 3},
+ "poll_option/1": {"meeting_user_id": 3},
+ "poll_ballot/1": {"acting_meeting_user_id": 3},
+ "poll_ballot/2": {"represented_meeting_user_id": 3},
}
for fqid, model in expected.items():
self.assert_model_exists(fqid, model)
diff --git a/tests/system/action/organization/test_delete_history_information.py b/tests/system/action/organization/test_delete_history_information.py
index 7177fadc47..6fab0196cd 100644
--- a/tests/system/action/organization/test_delete_history_information.py
+++ b/tests/system/action/organization/test_delete_history_information.py
@@ -1,3 +1,6 @@
+from datetime import datetime
+from zoneinfo import ZoneInfo
+
from openslides_backend.models.models import Poll
from openslides_backend.permissions.management_levels import OrganizationManagementLevel
from tests.system.action.base import BaseActionTestCase
@@ -13,6 +16,7 @@ def test_delete_history_information_no_permission(self) -> None:
def test_delete_history_information_correct(self) -> None:
self.create_meeting()
+ self.create_user_for_meeting(1)
self.set_models(
{
"organization/1": {"name": "Orga", "enable_electronic_voting": True},
@@ -25,25 +29,35 @@ def test_delete_history_information_correct(self) -> None:
"content_object_id": "assignment/1",
"meeting_id": 1,
},
+ "poll/1": {
+ "title": "test",
+ "meeting_id": 1,
+ "content_object_id": "assignment/1",
+ "visibility": Poll.VISIBILITY_MANUALLY,
+ "state": Poll.STATE_FINISHED,
+ "config_id": "poll_config_rating_approval/1",
+ "result": '{"yes": "3", "no": "2", "abstain": "1"}',
+ },
+ "poll_config_rating_approval/1": {
+ "poll_id": 1,
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID,
+ "allow_abstain": True,
+ },
+ "poll_option/1": {"poll_id": 1, "text": "Delete this item?"},
+ "history_position/1": {
+ "timestamp": datetime.fromtimestamp(1761760881, ZoneInfo("UTC")),
+ "original_user_id": 1,
+ "user_id": 1,
+ },
+ "history_entry/1": {
+ "position_id": 1,
+ "meeting_id": 1,
+ "entries": ["Ballot created"],
+ "original_model_id": "assignment/1",
+ "model_id": "assignment/1",
+ },
}
)
- vote_service_response = self.vote_service.create(
- {
- "title": "test",
- "visibility": Poll.VISIBILITY_MANUALLY,
- "method": Poll.METHOD_RATING_APPROVAL,
- "state": Poll.STATE_CREATED,
- "meeting_id": 1,
- "content_object_id": "assignment/1",
- "config": {"allow_abstain": True},
- "result": {"yes": "3", "no": "2", "abstain": "1"},
- },
- )
- self.assertIsNotNone(vote_service_response)
- self.assert_history_information("assignment/1", ["Ballot created"])
- for collection in ["history_position", "history_entry"]:
- self.assert_model_exists(f"{collection}/1")
- self.assert_model_not_exists(f"{collection}/2")
response = self.request("organization.delete_history_information", {"id": 1})
self.assert_status_code(response, 200)
diff --git a/tests/system/action/projector/test_toggle.py b/tests/system/action/projector/test_toggle.py
index 55fc86f6ef..f3f8af0c0b 100644
--- a/tests/system/action/projector/test_toggle.py
+++ b/tests/system/action/projector/test_toggle.py
@@ -22,8 +22,8 @@ def create_poll(self, base: int) -> None:
"content_object_id": "motion/1",
},
f"poll_config_rating_approval/{base}": {
- "poll_id": base,
"allow_abstain": False,
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID,
},
}
)
diff --git a/tests/system/action/test_migration_route.py b/tests/system/action/test_migration_route.py
index 940b84c4b3..e5633f9672 100644
--- a/tests/system/action/test_migration_route.py
+++ b/tests/system/action/test_migration_route.py
@@ -3,6 +3,8 @@
from typing import Any
from unittest.mock import Mock, patch
+import pytest
+
from openslides_backend.http.views.action_view import ActionView
from openslides_backend.migrations.exceptions import MigrationException
from openslides_backend.migrations.migration_handler import MigrationState
@@ -18,6 +20,7 @@
from .test_internal_actions import BaseInternalPasswordTest, BaseInternalRequestTest
+@pytest.mark.skip(reason="Waiting for the migration framework.")
class BaseMigrationRouteTest(BaseInternalRequestTest, BaseMigrationTestCase):
"""
Uses the anonymous client to call the migration route.
diff --git a/tests/system/action/user/test_delete.py b/tests/system/action/user/test_delete.py
index 15d17bc0fa..de43fc7078 100644
--- a/tests/system/action/user/test_delete.py
+++ b/tests/system/action/user/test_delete.py
@@ -191,7 +191,7 @@ def test_delete_with_submitter(self) -> None:
)
self.assert_model_exists("motion/50", {"submitter_ids": [34]})
- def test_delete_with_poll_candidate(self) -> None:
+ def test_delete_poll_option_meeting_user(self) -> None:
self.create_meeting()
user_id = self.create_user_for_meeting(1)
self.set_models(
@@ -213,46 +213,17 @@ def test_delete_with_poll_candidate(self) -> None:
"title": "Duckburg town council",
"meeting_id": 1,
},
- "poll_config_option/1": {
- "poll_config_id": "poll_config_approval/1",
- "meeting_user_id": 1,
+ "poll_option/1": {"poll_id": 1, "meeting_user_id": 1},
+ "poll_config_approval/1": {
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID,
},
- "poll_config_approval/1": {"poll_id": 1},
}
)
response = self.request("user.delete", {"id": user_id})
self.assert_status_code(response, 200)
self.assert_model_not_exists(f"user/{user_id}")
self.assert_model_not_exists("meeting_user/1")
- self.assert_model_exists("poll_config_option/1", {"meeting_user_id": None})
-
- def test_delete_poll_option_user(self) -> None:
- self.create_meeting()
- self.create_meeting(4)
- self.create_topic(1, 1)
- bob_id = self.create_user("bob", [1])
- self.set_models(
- {
- "poll/1": {
- "type": "named",
- "pollmethod": "Y",
- "backend": "long",
- "state": "finished",
- "meeting_id": 1,
- "content_object_id": "topic/1",
- "title": "Poll 1",
- "onehundred_percent_base": "YNA",
- },
- "option/1": {
- "meeting_id": 1,
- "poll_id": 1,
- "content_object_id": f"user/{bob_id}",
- },
- "option/2": {"meeting_id": 1, "poll_id": 1},
- }
- )
- response = self.request("user.delete", {"id": bob_id})
- self.assert_status_code(response, 200)
+ self.assert_model_exists("poll_option/1", {"meeting_user_id": None})
def test_delete_with_group_ids_set_null(self) -> None:
self.create_meeting()
diff --git a/tests/system/base.py b/tests/system/base.py
index 68a61baeb4..713c1dec3b 100644
--- a/tests/system/base.py
+++ b/tests/system/base.py
@@ -26,6 +26,7 @@
from openslides_backend.services.postgresql.db_connection_handling import (
get_new_os_conn,
)
+from openslides_backend.services.vote.interface import VoteService
from openslides_backend.shared.env import Environment
from openslides_backend.shared.exceptions import (
ActionException,
@@ -51,8 +52,6 @@
)
from tests.util import AuthData, Client, Response
-from .util import TestVoteService
-
DEFAULT_PASSWORD = "password"
ADMIN_USERNAME = "admin"
@@ -62,7 +61,7 @@
class BaseSystemTestCase(TestCase):
app: OpenSlidesBackendWSGIApplication
auth: AuthenticationService
- vote_service: TestVoteService
+ vote_service: VoteService
media: Any # Any is needed because it is mocked and has magic methods
client: Client
anon_client: Client
@@ -84,7 +83,7 @@ def setUp(self) -> None:
self.env = cast(Environment, self.app.env)
self.auth = self.services.authentication()
self.media = self.services.media()
- self.vote_service = cast(TestVoteService, self.services.vote())
+ self.vote_service = self.services.vote()
self.set_thread_watch_timeout(-1)
self.user_id = 1
diff --git a/tests/system/migrations/test_0100_init_reldb.py b/tests/system/migrations/test_0100_init_reldb.py
index 981a7aa4fa..5118bc07a4 100644
--- a/tests/system/migrations/test_0100_init_reldb.py
+++ b/tests/system/migrations/test_0100_init_reldb.py
@@ -12,6 +12,8 @@
from unittest.mock import Mock, patch
from zoneinfo import ZoneInfo
+import pytest
+
from meta.dev.src.generate_sql_schema import GenerateCodeBlocks
from openslides_backend.http.application import OpenSlidesBackendWSGIApplication
from openslides_backend.http.views import ActionView
@@ -64,6 +66,7 @@
data: dict[str, any] = {}
+@pytest.mark.skip(reason="Waiting for the migration framework.")
class TestMigration100(BaseMigrationTestCase):
"""
Commentary:
diff --git a/tests/system/presenter/test_check_database.py b/tests/system/presenter/test_check_database.py
index ab863f3e80..f6e4d37112 100644
--- a/tests/system/presenter/test_check_database.py
+++ b/tests/system/presenter/test_check_database.py
@@ -86,9 +86,7 @@ def get_meeting_defaults(self) -> dict[str, Any]:
"motion_poll_ballot_paper_selection": "CUSTOM_NUMBER",
"motion_poll_ballot_paper_number": 8,
"motion_poll_default_type": "pseudoanonymous",
- "motion_poll_default_method": "YNA",
- "motion_poll_default_onehundred_percent_base": "YNA",
- "motion_poll_default_backend": "fast",
+ "motion_poll_default_onehundred_percent_base": "valid",
"motion_poll_projection_name_order_first": "last_name",
"motion_poll_projection_max_columns": 6,
"users_enable_presence_view": False,
@@ -109,9 +107,8 @@ def get_meeting_defaults(self) -> dict[str, Any]:
"assignment_poll_default_type": "pseudoanonymous",
"assignment_poll_default_method": "Y",
"assignment_poll_default_onehundred_percent_base": "valid",
- "assignment_poll_default_backend": "fast",
"poll_default_type": "analog",
- "poll_default_onehundred_percent_base": "YNA",
+ "poll_default_onehundred_percent_base": "valid",
"poll_default_live_voting_enabled": False,
"poll_default_allow_invalid": False,
"poll_couple_countdown": True,
@@ -507,11 +504,13 @@ def test_correct_relations(self) -> None:
"meeting_id": 1,
"content_object_id": "motion/1",
"visibility": Poll.VISIBILITY_NAMED,
- "config_id": "poll_config_rating_approval/1",
+ "config_id": "poll_config_rating_approval/7",
"state": Poll.STATE_STARTED,
},
- "poll_config_rating_approval/7": {"poll_id": 7},
- "ballot/8": {
+ "poll_config_rating_approval/7": {
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID
+ },
+ "poll_ballot/8": {
"poll_id": 7,
"acting_meeting_user_id": 14,
"represented_meeting_user_id": 15,
diff --git a/tests/system/presenter/test_check_database_all.py b/tests/system/presenter/test_check_database_all.py
index e3e3be4e09..e5ede36915 100644
--- a/tests/system/presenter/test_check_database_all.py
+++ b/tests/system/presenter/test_check_database_all.py
@@ -92,9 +92,7 @@ def get_meeting_defaults(self) -> dict[str, Any]:
"motion_poll_ballot_paper_selection": "CUSTOM_NUMBER",
"motion_poll_ballot_paper_number": 8,
"motion_poll_default_type": "pseudoanonymous",
- "motion_poll_default_method": "YNA",
- "motion_poll_default_onehundred_percent_base": "YNA",
- "motion_poll_default_backend": "fast",
+ "motion_poll_default_onehundred_percent_base": "valid",
"motion_poll_projection_name_order_first": "last_name",
"motion_poll_projection_max_columns": 6,
"users_enable_presence_view": False,
@@ -116,9 +114,8 @@ def get_meeting_defaults(self) -> dict[str, Any]:
"assignment_poll_default_type": "pseudoanonymous",
"assignment_poll_default_method": "Y",
"assignment_poll_default_onehundred_percent_base": "valid",
- "assignment_poll_default_backend": "fast",
"poll_default_type": "analog",
- "poll_default_onehundred_percent_base": "YNA",
+ "poll_default_onehundred_percent_base": "valid",
"poll_default_live_voting_enabled": False,
"poll_default_allow_invalid": False,
"poll_couple_countdown": True,
@@ -583,11 +580,13 @@ def test_correct_relations(self) -> None:
"meeting_id": 1,
"content_object_id": "motion/1",
"visibility": Poll.VISIBILITY_NAMED,
- "config_id": "poll_config_rating_approval/1",
+ "config_id": "poll_config_rating_approval/7",
"state": Poll.STATE_STARTED,
},
- "poll_config_rating_approval/7": {"poll_id": 7},
- "ballot/8": {
+ "poll_config_rating_approval/7": {
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID
+ },
+ "poll_ballot/8": {
"poll_id": 7,
"acting_meeting_user_id": 14,
"represented_meeting_user_id": 15,
diff --git a/tests/system/presenter/test_export_meeting.py b/tests/system/presenter/test_export_meeting.py
index bfe5903e2c..80ad98d989 100644
--- a/tests/system/presenter/test_export_meeting.py
+++ b/tests/system/presenter/test_export_meeting.py
@@ -61,7 +61,6 @@ def test_correct(self) -> None:
"motion_block",
"motion_change_recommendation",
"poll",
- "vote",
"assignment",
"assignment_candidate",
"mediafile",
@@ -223,17 +222,15 @@ def test_export_meeting_find_special_users(self) -> None:
Find meeting_users in:
Collection | Field
poll | voted_ids
- motion | supporter_meeting_user_ids
ballot | acting_meeting_user_id
ballot | represented_meeting_user_id
- poll_config_option | meeting_user_id
+ poll_option | meeting_user_id
"""
- self.create_meeting()
self.create_motion(1, 30)
self.set_models(
{
"meeting/1": {"present_user_ids": [11]},
- "group/1": {"meeting_user_ids": [112, 114]},
+ "group/1": {"meeting_user_ids": [112, 113, 114]},
"user/11": {"username": "exuser11"},
"user/12": {"username": "exuser12"},
"user/13": {"username": "exuser13"},
@@ -245,27 +242,28 @@ def test_export_meeting_find_special_users(self) -> None:
"meeting_user_id": 112,
},
"meeting_user/113": {"meeting_id": 1, "user_id": 13},
- "meeting_user/114": {"meeting_id": 1, "user_id": 14},
+ "meeting_user/114": {
+ "meeting_id": 1,
+ "user_id": 14,
+ "poll_voted_ids": [80],
+ },
"poll/80": {
"title": "Poll 80",
"meeting_id": 1,
- "content_object_id": "assignment/10",
+ "content_object_id": "motion/30",
"visibility": Poll.VISIBILITY_NAMED,
"config_id": "poll_config_approval/90",
- "state": Poll.STATE_STARTED,
- "voted_ids": [114],
+ "state": Poll.STATE_FINISHED,
},
- "poll_config_approval/90": {"poll_id": 80},
- "poll_config_option/100": {
- "poll_config_id": "poll_config_approval/90",
- "meeting_user_id": 113,
+ "poll_config_approval/90": {
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID
},
- "ballot/120": {
+ "poll_option/100": {"poll_id": 80, "meeting_user_id": 113},
+ "poll_ballot/120": {
"poll_id": 80,
"value": "yes",
"represented_meeting_user_id": 114,
"acting_meeting_user_id": 114,
- "meeting_id": 1,
},
}
)
@@ -280,19 +278,122 @@ def test_export_meeting_find_special_users(self) -> None:
assert data["meeting"]["1"]["present_user_ids"] == [11]
assert data["user"]["11"]["is_present_in_meeting_ids"] == [1]
- assert data["motion"]["30"]["supporter_meeting_user_ids"] == [112]
- assert data["meeting_user"]["112"]["supported_motion_ids"] == [30]
-
- assert data["poll_config_option"]["100"]["meeting_user_id"] == 113
+ assert data["poll_option"]["100"]["meeting_user_id"] == 113
assert data["meeting_user"]["113"]["poll_option_ids"] == [100]
assert data["poll"]["80"]["voted_ids"] == [114]
assert data["meeting_user"]["114"]["poll_voted_ids"] == [80]
- assert data["poll_ballot"]["120"]["c"] == 114
+ assert data["poll_ballot"]["120"]["acting_meeting_user_id"] == 114
assert data["poll_ballot"]["120"]["represented_meeting_user_id"] == 114
assert data["meeting_user"]["114"]["acting_ballot_ids"] == [120]
assert data["meeting_user"]["114"]["represented_ballot_ids"] == [120]
+ def create_assignment(self, meeting_id: int, base: int) -> None:
+ self.set_models(
+ {
+ f"assignment/{base}": {
+ "title": "just do it",
+ "meeting_id": meeting_id,
+ },
+ f"list_of_speakers/{base + 100}": {
+ "content_object_id": f"assignment/{base}",
+ "meeting_id": meeting_id,
+ },
+ }
+ )
+
+ def test_export_meeting_find_poll_configs(self) -> None:
+ configs_data = {
+ "poll_config_approval/1": {
+ "poll_id": 11,
+ "allow_abstain": False,
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID,
+ },
+ "poll_config_selection/2": {
+ "poll_id": 12,
+ "max_options_amount": 1,
+ "min_options_amount": 1,
+ "allow_nota": True,
+ "strike_out": True,
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_ENTITLED,
+ },
+ "poll_config_rating_score/3": {
+ "poll_id": 13,
+ "min_options_amount": 1,
+ "max_options_amount": 2,
+ "max_votes_per_option": 3,
+ "min_vote_sum": 1,
+ "max_vote_sum": 3,
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_ENTITLED_PRESENT,
+ },
+ "poll_config_rating_approval/4": {
+ "poll_id": 14,
+ "min_options_amount": 1,
+ "max_options_amount": 3,
+ "allow_abstain": False,
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_ENTITLED_PRESENT,
+ },
+ }
+ self.create_motion(1, 21)
+ self.create_motion(1, 24)
+ self.create_assignment(1, 22)
+ self.create_assignment(1, 23)
+ self.create_user_for_meeting(1)
+ self.create_user_for_meeting(1)
+
+ self.set_models(
+ {
+ **configs_data,
+ "poll/11": {
+ "title": "Poll 11",
+ "meeting_id": 1,
+ "content_object_id": "motion/21",
+ "config_id": "poll_config_approval/1",
+ "visibility": Poll.VISIBILITY_NAMED,
+ "state": Poll.STATE_CREATED,
+ },
+ "poll/12": {
+ "title": "Poll 12",
+ "meeting_id": 1,
+ "content_object_id": "assignment/22",
+ "config_id": "poll_config_selection/2",
+ "visibility": Poll.VISIBILITY_OPEN,
+ "state": Poll.STATE_STARTED,
+ },
+ "poll/13": {
+ "title": "Poll 13",
+ "meeting_id": 1,
+ "content_object_id": "assignment/23",
+ "config_id": "poll_config_rating_score/3",
+ "visibility": Poll.VISIBILITY_SECRET,
+ "state": Poll.STATE_FINISHED,
+ },
+ "poll/14": {
+ "title": "Poll 14",
+ "meeting_id": 1,
+ "content_object_id": "motion/24",
+ "config_id": "poll_config_rating_approval/4",
+ "visibility": Poll.VISIBILITY_NAMED,
+ "state": Poll.STATE_STARTED,
+ },
+ "poll_option/121": {"poll_id": 12, "meeting_user_id": 1},
+ "poll_option/122": {"poll_id": 12, "meeting_user_id": 2},
+ "poll_option/131": {"poll_id": 13, "meeting_user_id": 1},
+ "poll_option/132": {"poll_id": 13, "meeting_user_id": 2},
+ "poll_option/141": {"poll_id": 14, "text": "Blue"},
+ "poll_option/142": {"poll_id": 14, "text": "Green"},
+ "poll_option/143": {"poll_id": 14, "text": "Red"},
+ "group/1": {"poll_ids": [11, 12, 13, 14]},
+ }
+ )
+ status_code, data = self.request("export_meeting", {"meeting_id": 1})
+ assert status_code == 200
+ for config_fqid, config_data in configs_data.items():
+ config_collection, config_id = config_fqid.split("/")
+ assert data["poll"][str(config_data["poll_id"])]["config_id"] == config_fqid
+ for k, v in config_data.items():
+ assert data[config_collection][config_id][k] == v
+
def test_with_structured_published_orga_files(self) -> None:
self.set_models(
{
@@ -380,9 +481,7 @@ def test_with_structured_published_orga_files(self) -> None:
"motion_poll_ballot_paper_selection": "CUSTOM_NUMBER",
"motion_poll_ballot_paper_number": 8,
"motion_poll_default_type": "pseudoanonymous",
- "motion_poll_default_method": "YNA",
- "motion_poll_default_onehundred_percent_base": "YNA",
- "motion_poll_default_backend": "fast",
+ "motion_poll_default_onehundred_percent_base": "valid",
"motion_poll_projection_name_order_first": "last_name",
"motion_poll_projection_max_columns": 6,
"users_enable_presence_view": False,
@@ -403,10 +502,10 @@ def test_with_structured_published_orga_files(self) -> None:
"assignment_poll_default_type": "pseudoanonymous",
"assignment_poll_default_method": "Y",
"assignment_poll_default_onehundred_percent_base": "valid",
- "assignment_poll_default_backend": "fast",
"poll_default_type": "analog",
- "poll_default_onehundred_percent_base": "YNA",
+ "poll_default_onehundred_percent_base": "valid",
"poll_default_live_voting_enabled": False,
+ "poll_default_method": "YNA",
"poll_couple_countdown": True,
},
"group/1": {
diff --git a/tests/system/util.py b/tests/system/util.py
index 6fed3c97b8..a169287f45 100644
--- a/tests/system/util.py
+++ b/tests/system/util.py
@@ -1,6 +1,5 @@
import cProfile
import os
-from abc import abstractmethod
from collections.abc import Callable
from typing import Any, Self
from unittest.mock import MagicMock, Mock, patch
@@ -15,7 +14,6 @@
from openslides_backend.services.database.extended_database import ExtendedDatabase
from openslides_backend.services.media.interface import MediaService
from openslides_backend.services.vote.adapter import VoteAdapter
-from openslides_backend.services.vote.interface import VoteService
from openslides_backend.shared.env import Environment, is_truthy
from openslides_backend.shared.exceptions import MediaServiceException
from openslides_backend.shared.interfaces.services import Services
@@ -34,19 +32,6 @@ def convert_to_test_response(response: RequestsResponse) -> Response:
)
-class TestVoteService(VoteService):
- url: str
-
- @abstractmethod
- def vote(self, poll_id: int, payload: dict[str, Any]) -> Response: ...
-
-
-class TestVoteAdapter(VoteAdapter, TestVoteService):
- def vote(self, poll_id: int, payload: dict[str, Any]) -> Response:
- endpoint = self.url + f"?id={poll_id}"
- return self.retrieve(endpoint, payload)
-
-
def create_action_test_application() -> OpenSlidesBackendWSGIApplication:
return create_test_application(ActionView)
@@ -75,7 +60,7 @@ def create_test_application(view: type[View]) -> OpenSlidesBackendWSGIApplicatio
logging=MagicMock(),
)
services.vote = providers.Singleton(
- TestVoteAdapter, services.config.vote_url, MagicMock()
+ VoteAdapter, services.config.vote_url, MagicMock()
)
mock_media_service = Mock(MediaService)
mock_media_service.upload_mediafile = Mock(
From 0e6f783f69f72d8de314757c8ced5e0e359f4d1e Mon Sep 17 00:00:00 2001
From: Viktoriia Krasnovyd <114735598+vkrasnovyd@users.noreply.github.com>
Date: Fri, 26 Jun 2026 10:55:40 +0200
Subject: [PATCH 15/23] [vote] Don't delete group with running poll (#3618)
---
meta | 2 +-
.../action/actions/group/delete.py | 16 +++++++-
openslides_backend/models/models.py | 8 +++-
tests/database/reader/system/test_filter.py | 1 +
tests/system/action/group/test_delete.py | 40 +++++++++++++++++++
5 files changed, 62 insertions(+), 5 deletions(-)
diff --git a/meta b/meta
index d9cc01053a..221c498ec1 160000
--- a/meta
+++ b/meta
@@ -1 +1 @@
-Subproject commit d9cc01053a0f230a312df6100fd6dd0dff17d2c9
+Subproject commit 221c498ec118fab073e4da116637bde63ffd960c
diff --git a/openslides_backend/action/actions/group/delete.py b/openslides_backend/action/actions/group/delete.py
index c9e292b136..c99c8a26a4 100644
--- a/openslides_backend/action/actions/group/delete.py
+++ b/openslides_backend/action/actions/group/delete.py
@@ -4,10 +4,10 @@
from openslides_backend.shared.exceptions import ActionException
-from ....models.models import Group
+from ....models.models import Group, Poll
from ....permissions.permissions import Permissions
from ....services.database.commands import GetManyRequest
-from ....shared.filters import And, FilterOperator
+from ....shared.filters import And, FilterOperator, Or
from ....shared.interfaces.event import Event, EventType, ListFields
from ....shared.patterns import (
FullQualifiedId,
@@ -41,12 +41,24 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
"meeting_mediafile_inherited_access_group_ids",
"meeting_user_ids",
"meeting_id",
+ "poll_ids",
],
)
if len(group.get("meeting_user_ids", [])) and not self.is_meeting_to_be_deleted(
group["meeting_id"]
):
raise ActionException("You cannot delete a group with users.")
+ if poll_ids := group.get("poll_ids", []):
+ running_polls = self.datastore.filter(
+ "poll",
+ And(
+ FilterOperator("state", "=", Poll.STATE_STARTED),
+ Or(FilterOperator("id", "=", poll_id) for poll_id in poll_ids),
+ ),
+ ["id"],
+ )
+ if running_polls:
+ raise ActionException("You cannot delete a group with a running poll.")
self.meeting_mediafile_ids: list[int] = list(
set(group.get("meeting_mediafile_access_group_ids", []))
| set(group.get("meeting_mediafile_inherited_access_group_ids", []))
diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py
index d0047f5fbd..1429a7248f 100644
--- a/openslides_backend/models/models.py
+++ b/openslides_backend/models/models.py
@@ -538,9 +538,10 @@ class HistoryEntry(Model):
original_model_id = fields.CharField(constant=True)
model_id = fields.GenericRelationField(
to={
- "user": "history_entry_ids",
- "motion": "history_entry_ids",
"assignment": "history_entry_ids",
+ "motion": "history_entry_ids",
+ "poll": "history_entry_ids",
+ "user": "history_entry_ids",
}
)
position_id = fields.RelationField(
@@ -2408,6 +2409,9 @@ class Poll(Model, PollModelMixin):
meeting_id = fields.RelationField(
to={"meeting": "poll_ids"}, required=True, constant=True
)
+ history_entry_ids = fields.RelationListField(
+ to={"history_entry": "model_id"}, is_view_field=True
+ )
class PollBallot(Model):
diff --git a/tests/database/reader/system/test_filter.py b/tests/database/reader/system/test_filter.py
index 1d466ce299..7f83e16244 100644
--- a/tests/database/reader/system/test_filter.py
+++ b/tests/database/reader/system/test_filter.py
@@ -180,6 +180,7 @@ def test_types_str_list(db_connection: Connection) -> None:
"model_id": "user/1",
"model_id_assignment_id": None,
"model_id_motion_id": None,
+ "model_id_poll_id": None,
"model_id_user_id": 1,
"original_model_id": None,
"position_id": 1,
diff --git a/tests/system/action/group/test_delete.py b/tests/system/action/group/test_delete.py
index 67f1e67664..09f81c8d91 100644
--- a/tests/system/action/group/test_delete.py
+++ b/tests/system/action/group/test_delete.py
@@ -1,3 +1,4 @@
+from openslides_backend.models.models import Poll
from openslides_backend.permissions.permissions import Permissions
from tests.system.action.base import BaseActionTestCase
@@ -59,6 +60,45 @@ def test_delete_with_users(self) -> None:
response.json["message"], "You cannot delete a group with users."
)
+ def base_test_delete_with_poll(self, poll_state: str, fail: bool) -> None:
+ self.create_motion(22, 30)
+ self.set_models(
+ {
+ "poll/71": {
+ "meeting_id": 22,
+ "title": "Poll 71",
+ "visibility": Poll.VISIBILITY_SECRET,
+ "config_id": "poll_config_approval/34",
+ "state": poll_state,
+ "content_object_id": "motion/30",
+ },
+ "poll_config_approval/34": {
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID
+ },
+ "group/24": {"poll_ids": [71]},
+ }
+ )
+ response = self.request("group.delete", {"id": 24})
+
+ if fail:
+ self.assert_status_code(response, 400)
+ self.assertEqual(
+ response.json["message"],
+ "You cannot delete a group with a running poll.",
+ )
+ else:
+ self.assert_status_code(response, 200)
+ self.assert_model_not_exists("group/24")
+
+ def test_delete_with_poll_created(self) -> None:
+ self.base_test_delete_with_poll(Poll.STATE_CREATED, False)
+
+ def test_delete_with_poll_started(self) -> None:
+ self.base_test_delete_with_poll(Poll.STATE_STARTED, True)
+
+ def test_delete_with_poll_finished(self) -> None:
+ self.base_test_delete_with_poll(Poll.STATE_FINISHED, False)
+
def test_delete_no_permissions(self) -> None:
self.base_permission_test(
{"group/4": {"name": "name_srtgb123", "meeting_id": 1}},
From ce36e4e77900f8cbfae0ed630a9f068b03c1c6a9 Mon Sep 17 00:00:00 2001
From: Bastian Rihm
Date: Wed, 1 Jul 2026 13:43:57 +0200
Subject: [PATCH 16/23] Update meta (#3646)
---
meta | 2 +-
openslides_backend/models/models.py | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/meta b/meta
index 221c498ec1..ec166875b6 160000
--- a/meta
+++ b/meta
@@ -1 +1 @@
-Subproject commit 221c498ec118fab073e4da116637bde63ffd960c
+Subproject commit ec166875b618c19810cec43e934284f026efb9ed
diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py
index 1429a7248f..bbcfe6c07b 100644
--- a/openslides_backend/models/models.py
+++ b/openslides_backend/models/models.py
@@ -2470,6 +2470,7 @@ class PollConfigRatingApproval(Model):
)
max_options_amount = fields.IntegerField(default=0)
min_options_amount = fields.IntegerField(default=0)
+ max_yes_amount = fields.IntegerField()
allow_abstain = fields.BooleanField(default=True)
onehundred_percent_base = fields.CharField(
required=True,
From 994920eb5356414bf8f5105af9d07f197657366a Mon Sep 17 00:00:00 2001
From: Viktoriia Krasnovyd <114735598+vkrasnovyd@users.noreply.github.com>
Date: Fri, 10 Jul 2026 17:23:35 +0200
Subject: [PATCH 17/23] [vote] Write changed user fields in history_entry
(#3659)
---
meta | 2 +-
openslides_backend/action/action.py | 12 ++-
.../actions/meeting_user/base_delete.py | 13 ++-
.../action/actions/meeting_user/create.py | 17 +--
.../actions/meeting_user/history_mixin.py | 101 ++++++++++++++----
.../actions/motion/base_create_forwarded.py | 24 +++--
.../action/actions/motion/delete.py | 30 ++++--
.../motion_state_history_information_mixin.py | 15 ++-
.../action/actions/motion/update.py | 25 ++---
.../motion_comment/create_delete_update.py | 17 +--
.../action/actions/user/merge_mixins.py | 29 ++---
.../action/actions/user/merge_together.py | 21 ++--
.../action/actions/user/set_present.py | 34 ++++--
.../action/actions/user/user_mixins.py | 23 ++--
openslides_backend/models/models.py | 1 +
openslides_backend/shared/history_events.py | 59 +++++++++-
openslides_backend/shared/patterns.py | 2 +-
openslides_backend/shared/typing.py | 12 ++-
tests/database/reader/system/test_filter.py | 1 +
tests/system/action/base.py | 34 +++---
tests/system/action/user/test_create.py | 1 +
.../action/user/test_delegation_history.py | 18 +++-
tests/system/action/user/test_set_present.py | 43 +++++---
tests/system/action/user/test_update.py | 9 ++
24 files changed, 395 insertions(+), 148 deletions(-)
diff --git a/meta b/meta
index ec166875b6..d16e66eb6a 160000
--- a/meta
+++ b/meta
@@ -1 +1 @@
-Subproject commit ec166875b618c19810cec43e934284f026efb9ed
+Subproject commit d16e66eb6a061264f5c799df79556e9efe7643d6
diff --git a/openslides_backend/action/action.py b/openslides_backend/action/action.py
index 2e32659e16..63a6d2f59d 100644
--- a/openslides_backend/action/action.py
+++ b/openslides_backend/action/action.py
@@ -9,6 +9,7 @@
from psycopg.types.json import Jsonb
from openslides_backend.shared.base_service_provider import BaseServiceProvider
+from openslides_backend.shared.history_events import update_history_information_multi
from ..models.base import Model, model_registry
from ..models.fields import BaseRelationField, GenericRelationField
@@ -540,7 +541,7 @@ def get_history_information(self) -> HistoryInformation | None:
if self.history_information is None:
return None
- information = {}
+ information: HistoryInformation = {}
instances = (
self.get_instances_with_fields(["id", self.history_relation_field])
if self.history_relation_field
@@ -558,8 +559,9 @@ def get_history_information(self) -> HistoryInformation | None:
fqids.append(
fqid_from_collection_and_id(self.model.collection, instance["id"])
)
- for fqid in fqids:
- information[fqid] = [self.history_information]
+ update_history_information_multi(
+ information, fqids, [self.history_information]
+ )
return information
def get_instances_with_fields(
@@ -777,7 +779,9 @@ def merge_history_informations(
b = {}
for fqid, information in b.items():
if fqid in a:
- a[fqid].extend(information)
+ a[fqid]["entries"].extend(information["entries"])
+ if changed_fields := information.get("changed_fields"):
+ a[fqid].setdefault("changed_fields", dict()).update(changed_fields)
else:
a[fqid] = information
return a
diff --git a/openslides_backend/action/actions/meeting_user/base_delete.py b/openslides_backend/action/actions/meeting_user/base_delete.py
index 171deb21c0..da1b06eac5 100644
--- a/openslides_backend/action/actions/meeting_user/base_delete.py
+++ b/openslides_backend/action/actions/meeting_user/base_delete.py
@@ -1,3 +1,4 @@
+from openslides_backend.shared.history_events import build_history_information_data
from openslides_backend.shared.patterns import fqid_from_collection_and_id
from openslides_backend.shared.typing import HistoryInformation
@@ -17,9 +18,13 @@ class MeetingUserBaseDelete(DeleteAction):
def get_history_information(self) -> HistoryInformation | None:
users = self.get_instances_with_fields(["user_id", "meeting_id"])
return {
- fqid_from_collection_and_id("user", user["user_id"]): [
- "Participant removed from meeting {}",
- fqid_from_collection_and_id("meeting", user["meeting_id"]),
- ]
+ fqid_from_collection_and_id(
+ "user", user["user_id"]
+ ): build_history_information_data(
+ [
+ "Participant removed from meeting {}",
+ fqid_from_collection_and_id("meeting", user["meeting_id"]),
+ ],
+ )
for user in users
}
diff --git a/openslides_backend/action/actions/meeting_user/create.py b/openslides_backend/action/actions/meeting_user/create.py
index f71b2edf6c..07168f1471 100644
--- a/openslides_backend/action/actions/meeting_user/create.py
+++ b/openslides_backend/action/actions/meeting_user/create.py
@@ -1,6 +1,7 @@
from typing import Any
from openslides_backend.shared.exceptions import ActionException
+from openslides_backend.shared.history_events import update_history_information
from openslides_backend.shared.patterns import fqid_from_collection_and_id
from openslides_backend.shared.typing import HistoryInformation
@@ -51,9 +52,9 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
return super().update_instance(instance)
def get_history_information(self) -> HistoryInformation | None:
- information = {}
+ information: HistoryInformation = {}
for instance in self.instances:
- instance_information = []
+ entries = []
fqids_per_collection = {
collection_name: [
fqid_from_collection_and_id(
@@ -65,15 +66,17 @@ def get_history_information(self) -> HistoryInformation | None:
for collection_name in ["group", "structure_level"]
if (ids := instance.get(f"{collection_name}_ids"))
}
- instance_information.append(
+ entries.append(
self.compose_history_string(list(fqids_per_collection.items()))
)
for collection_name, fqids in fqids_per_collection.items():
- instance_information.extend(fqids)
- instance_information.append(
+ entries.extend(fqids)
+ entries.append(
fqid_from_collection_and_id("meeting", instance["meeting_id"]),
)
- information[fqid_from_collection_and_id("user", instance["user_id"])] = (
- instance_information
+ update_history_information(
+ information,
+ fqid_from_collection_and_id("user", instance["user_id"]),
+ entries,
)
return information
diff --git a/openslides_backend/action/actions/meeting_user/history_mixin.py b/openslides_backend/action/actions/meeting_user/history_mixin.py
index 76a33e3263..134c8e0e92 100644
--- a/openslides_backend/action/actions/meeting_user/history_mixin.py
+++ b/openslides_backend/action/actions/meeting_user/history_mixin.py
@@ -1,16 +1,26 @@
from collections.abc import Iterable
from copy import deepcopy
-from typing import Any
+from typing import Any, NotRequired, TypedDict
from openslides_backend.action.mixins.extend_history_mixin import ExtendHistoryMixin
+from openslides_backend.shared.filters import FilterOperator
+from openslides_backend.shared.history_events import build_history_information_data
from openslides_backend.shared.interfaces.event import Event, EventType
from ....services.database.interface import GetManyRequest
-from ....shared.patterns import fqid_from_collection_and_id
+from ....shared.patterns import Field, FullQualifiedId, fqid_from_collection_and_id
from ....shared.typing import HistoryInformation
from ...action import Action
+class ActionHistoryInformationData(TypedDict):
+ entries: list[tuple[str, ...]]
+ changed_fields: NotRequired[dict[Field, Any]]
+
+
+ActionHistoryInformation = dict[FullQualifiedId, ActionHistoryInformationData]
+
+
class MeetingUserHistoryMixin(ExtendHistoryMixin, Action):
extend_history_to = "user_id"
@@ -76,7 +86,7 @@ def create_events(self, instance: dict[str, Any]) -> Iterable[Event]:
)
def get_history_information(self) -> HistoryInformation | None:
- information: dict[str, list[tuple[str, ...]]] = {}
+ information: ActionHistoryInformation = {}
# Scan the instances and collect the info for the history information
# Copy instances first since they are modified
@@ -97,7 +107,10 @@ def get_history_information(self) -> HistoryInformation | None:
)
return {
- fqid: [string for entry in history for string in entry]
+ fqid: build_history_information_data(
+ [string for entry in history.get("entries", []) for string in entry],
+ history.get("changed_fields", {}),
+ )
for fqid, history in information.items()
}
@@ -105,9 +118,10 @@ def add_updated_meeting_user_history_information(
self,
instance: dict[str, Any],
db_instance: dict[str, Any],
- information: dict[str, list[tuple[str, ...]]],
+ information: ActionHistoryInformation,
) -> None:
- instance_information: list[tuple[str, ...]] = []
+ instance_entries: list[tuple[str, ...]] = []
+ instance_changed_fields: dict[str, list[int]] = {}
user_id = db_instance["user_id"]
meeting_id = db_instance["meeting_id"]
@@ -120,25 +134,33 @@ def add_updated_meeting_user_history_information(
# meeting specific data
update_fields = ["structure_level_ids", "number", "vote_weight"]
if any(field in instance for field in update_fields):
- instance_information.append(
+ instance_entries.append(
(
"Participant data updated in meeting {}",
fqid_from_collection_and_id("meeting", meeting_id),
)
)
- self.handle_group_updates(instance_information, instance, db_instance)
- self.handle_delegations(
- information, instance_information, instance, db_instance
+ self.handle_group_updates(
+ instance_entries,
+ instance_changed_fields,
+ instance,
+ db_instance,
)
+ self.handle_delegations(information, instance_entries, instance, db_instance)
- if instance_information:
+ if instance_entries or instance_changed_fields:
self.add_entries_to_history_information(
- information, instance_information, for_user_id=user_id
+ information,
+ instance_entries,
+ for_user_id=user_id,
+ changed_fields=instance_changed_fields,
)
def add_created_meeting_user_history_information(
- self, instance: dict[str, Any], information: dict[str, list[tuple[str, ...]]]
+ self,
+ instance: dict[str, Any],
+ information: ActionHistoryInformation,
) -> None:
db_instance = self.datastore.get(
fqid_from_collection_and_id(self.model.collection, instance["id"]),
@@ -175,15 +197,21 @@ def add_created_meeting_user_history_information(
if instance_information:
self.add_entries_to_history_information(
- information, instance_information, for_user_id=db_instance["user_id"]
+ information,
+ instance_information,
+ for_user_id=db_instance["user_id"],
+ changed_fields={
+ "group_ids": self.get_changed_group_ids(db_instance["user_id"])
+ },
)
def add_entries_to_history_information(
self,
- information: dict[str, list[tuple[str, ...]]],
+ information: ActionHistoryInformation,
entries: list[tuple[str, ...]],
for_user_id: int | None = None,
for_meeting_user_id: int | None = None,
+ changed_fields: dict[Field, Any] | None = None,
) -> None:
if not for_user_id:
if not for_meeting_user_id:
@@ -197,11 +225,17 @@ def add_entries_to_history_information(
user_id = for_user_id
fqid = fqid_from_collection_and_id("user", user_id)
if fqid not in information:
- information[fqid] = entries
+ information[fqid] = {"entries": entries}
+ if changed_fields:
+ information[fqid]["changed_fields"] = changed_fields
else:
for entry in entries:
- if entry not in information[fqid]:
- information[fqid].append(entry)
+ if entry not in information[fqid]["entries"]:
+ information[fqid]["entries"].append(entry)
+ if changed_fields:
+ information[fqid].setdefault("changed_fields", dict()).update(
+ changed_fields
+ )
def compose_history_string(
self, fqids_per_collection: list[tuple[str, list[str]]]
@@ -231,7 +265,8 @@ def compose_history_string(
def handle_group_updates(
self,
- instance_information: list[tuple[str, ...]],
+ entries: list[tuple[str, ...]],
+ changed_fields: dict[str, list[int]],
instance: dict[str, Any],
db_instance: dict[str, Any],
) -> None:
@@ -242,6 +277,11 @@ def handle_group_updates(
added = instance_group_ids - db_group_ids
removed = db_group_ids - instance_group_ids
+ if added or removed:
+ changed_fields["group_ids"] = self.get_changed_group_ids(
+ db_instance["user_id"], {meeting_id: instance["group_ids"]}
+ )
+
# remove default groups
meeting = self.datastore.get(
fqid_from_collection_and_id("meeting", meeting_id),
@@ -273,11 +313,11 @@ def handle_group_updates(
fqid_from_collection_and_id("meeting", meeting_id)
)
if group_information:
- instance_information.append(tuple(group_information))
+ entries.append(tuple(group_information))
def handle_delegations(
self,
- information: dict[str, list[tuple[str, ...]]],
+ information: ActionHistoryInformation,
instance_information: list[tuple[str, ...]],
instance: dict[str, Any],
db_instance: dict[str, Any],
@@ -447,3 +487,22 @@ def handle_delegations(
],
for_meeting_user_id=muser_id,
)
+
+ def get_changed_group_ids(
+ self, user_id: int, update_data: dict[int, list[int]] = {}
+ ) -> list[int]:
+ db_groups: dict[int, dict[str, Any]] = self.datastore.filter(
+ "meeting_user",
+ FilterOperator("user_id", "=", user_id),
+ ["meeting_id", "group_ids"],
+ lock_result=False,
+ )
+ changed_groups = [
+ (
+ updated_groups
+ if (updated_groups := update_data.get(db_data["meeting_id"]))
+ else db_data["group_ids"]
+ )
+ for db_data in db_groups.values()
+ ]
+ return [id_ for group_ids in changed_groups for id_ in group_ids]
diff --git a/openslides_backend/action/actions/motion/base_create_forwarded.py b/openslides_backend/action/actions/motion/base_create_forwarded.py
index c5a808bad7..6d675e7c0d 100644
--- a/openslides_backend/action/actions/motion/base_create_forwarded.py
+++ b/openslides_backend/action/actions/motion/base_create_forwarded.py
@@ -6,6 +6,7 @@
from psycopg.types.json import Jsonb
from openslides_backend.action.actions.motion.mixins import TextHashMixin
+from openslides_backend.shared.history_events import update_history_information
from openslides_backend.shared.typing import HistoryInformation
from ....i18n.translator import Translator
@@ -641,22 +642,23 @@ def forward_mediafiles(
return instance
def get_history_information(self) -> HistoryInformation | None:
- forwarded_entries = defaultdict(list)
+ information: HistoryInformation = {}
for instance in self.instances:
- forwarded_entries[
- fqid_from_collection_and_id("motion", instance["origin_id"])
- ].extend(
+ update_history_information(
+ information,
+ fqid_from_collection_and_id("motion", instance["origin_id"]),
[
"Forwarded to {}",
fqid_from_collection_and_id("meeting", instance["meeting_id"]),
- ]
+ ],
)
- return forwarded_entries | {
- fqid_from_collection_and_id("motion", instance["id"]): [
- "Motion created (forwarded)"
- ]
- for instance in self.instances
- }
+ for instance in self.instances:
+ update_history_information(
+ information,
+ fqid_from_collection_and_id("motion", instance["id"]),
+ ["Motion created (forwarded)"],
+ )
+ return information
def check_can_forward_with_attachments(self) -> None:
organization = self.datastore.get(
diff --git a/openslides_backend/action/actions/motion/delete.py b/openslides_backend/action/actions/motion/delete.py
index fd1f9883c5..0b8e18d7fd 100644
--- a/openslides_backend/action/actions/motion/delete.py
+++ b/openslides_backend/action/actions/motion/delete.py
@@ -2,6 +2,10 @@
from typing import Any
from openslides_backend.action.action import merge_history_informations
+from openslides_backend.shared.history_events import (
+ build_history_information_data,
+ update_history_information_multi,
+)
from openslides_backend.shared.typing import HistoryInformation
from ....models.models import Motion
@@ -76,14 +80,16 @@ def get_history_information(self) -> HistoryInformation | None:
if self.history_information is None:
return information
# generate the history informations for the deleted amendments
- fqids = [
+ if not information:
+ information = {}
+ all_fqids = [
fqid_from_collection_and_id("motion", id_) for id_ in self.all_motion_ids
]
- if not information:
- information = {fqid: [self.history_information] for fqid in fqids}
- else:
- for fqid in fqids:
- information[fqid] = [self.history_information]
+ update_history_information_multi(
+ information,
+ [fqid for fqid in all_fqids if fqid not in information],
+ [self.history_information],
+ )
return information
def get_full_history_information(self) -> HistoryInformation | None:
@@ -97,12 +103,20 @@ def get_full_history_information(self) -> HistoryInformation | None:
return merge_history_informations(
information or {},
{
- fqid_from_collection_and_id("motion", id): ["Forwarded motion deleted"]
+ fqid_from_collection_and_id(
+ "motion", id
+ ): build_history_information_data(
+ ["Forwarded motion deleted"],
+ )
for instance in instances
for id in instance.get("all_origin_ids", [])
},
{
- fqid_from_collection_and_id("motion", id): ["Origin motion deleted"]
+ fqid_from_collection_and_id(
+ "motion", id
+ ): build_history_information_data(
+ ["Origin motion deleted"],
+ )
for instance in instances
for id in instance.get("all_derived_motion_ids", [])
},
diff --git a/openslides_backend/action/actions/motion/motion_state_history_information_mixin.py b/openslides_backend/action/actions/motion/motion_state_history_information_mixin.py
index 11ea727bcb..5273101619 100644
--- a/openslides_backend/action/actions/motion/motion_state_history_information_mixin.py
+++ b/openslides_backend/action/actions/motion/motion_state_history_information_mixin.py
@@ -1,3 +1,4 @@
+from openslides_backend.shared.history_events import build_history_information_data
from openslides_backend.shared.patterns import fqid_from_collection_and_id
from openslides_backend.shared.typing import HistoryInformation
@@ -9,9 +10,15 @@ def _get_state_history_information(
self, instance_field: str, verbose_model: str
) -> HistoryInformation:
return {
- fqid_from_collection_and_id(self.model.collection, instance["id"]): [
- verbose_model + " set to {}",
- fqid_from_collection_and_id("motion_state", instance[instance_field]),
- ]
+ fqid_from_collection_and_id(
+ self.model.collection, instance["id"]
+ ): build_history_information_data(
+ [
+ verbose_model + " set to {}",
+ fqid_from_collection_and_id(
+ "motion_state", instance[instance_field]
+ ),
+ ],
+ )
for instance in self.instances
}
diff --git a/openslides_backend/action/actions/motion/update.py b/openslides_backend/action/actions/motion/update.py
index b5884a759a..27bde7caa2 100644
--- a/openslides_backend/action/actions/motion/update.py
+++ b/openslides_backend/action/actions/motion/update.py
@@ -4,6 +4,7 @@
from psycopg.types.json import Jsonb
+from openslides_backend.shared.history_events import update_history_information
from openslides_backend.shared.typing import HistoryInformation
from ....models.models import Motion
@@ -223,19 +224,17 @@ def check_permissions(self, instance: dict[str, Any]) -> None:
raise PermissionDenied(msg)
def get_history_information(self) -> HistoryInformation | None:
- information = {}
+ information: HistoryInformation = {}
for instance in deepcopy(self.instances):
- instance_information = []
+ entries = []
# workflow timestamp changed
if "workflow_timestamp" in instance:
timestamp = instance.pop("workflow_timestamp")
- instance_information.extend(
- ["Workflow_timestamp set to {}", f"{timestamp}"]
- )
+ entries.extend(["Workflow_timestamp set to {}", f"{timestamp}"])
# category changed
- instance_information.extend(
+ entries.extend(
self.create_history_information_for_field(
instance,
"category_id",
@@ -245,7 +244,7 @@ def get_history_information(self) -> HistoryInformation | None:
)
# block changed
- instance_information.extend(
+ entries.extend(
self.create_history_information_for_field(
instance, "block_id", "motion_block", "Motion block"
)
@@ -263,12 +262,14 @@ def get_history_information(self) -> HistoryInformation | None:
]
if any(field in instance for field in generic_update_fields):
# still other fields given, so we also add the generic "updated" message
- instance_information.append("Motion updated")
+ entries.append("Motion updated")
- if instance_information:
- information[
- fqid_from_collection_and_id(self.model.collection, instance["id"])
- ] = instance_information
+ if entries:
+ update_history_information(
+ information,
+ fqid_from_collection_and_id(self.model.collection, instance["id"]),
+ entries,
+ )
return information
diff --git a/openslides_backend/action/actions/motion_comment/create_delete_update.py b/openslides_backend/action/actions/motion_comment/create_delete_update.py
index eb23817b89..4141659144 100644
--- a/openslides_backend/action/actions/motion_comment/create_delete_update.py
+++ b/openslides_backend/action/actions/motion_comment/create_delete_update.py
@@ -2,6 +2,7 @@
from openslides_backend.action.mixins.extend_history_mixin import ExtendHistoryMixin
from openslides_backend.permissions.management_levels import OrganizationManagementLevel
+from openslides_backend.shared.history_events import build_history_information_data
from openslides_backend.shared.typing import HistoryInformation
from ....models.models import MotionComment
@@ -103,12 +104,16 @@ def get_history_information(self) -> HistoryInformation | None:
instances = self.get_instances_with_fields(["motion_id", "section_id"])
_, action = self.name.split(".")
return {
- fqid_from_collection_and_id("motion", instance["motion_id"]): [
- "Comment {} " + action + "d",
- fqid_from_collection_and_id(
- "motion_comment_section", instance["section_id"]
- ),
- ]
+ fqid_from_collection_and_id(
+ "motion", instance["motion_id"]
+ ): build_history_information_data(
+ [
+ "Comment {} " + action + "d",
+ fqid_from_collection_and_id(
+ "motion_comment_section", instance["section_id"]
+ ),
+ ],
+ )
for instance in instances
}
diff --git a/openslides_backend/action/actions/user/merge_mixins.py b/openslides_backend/action/actions/user/merge_mixins.py
index e47fce259e..ee260e07bb 100644
--- a/openslides_backend/action/actions/user/merge_mixins.py
+++ b/openslides_backend/action/actions/user/merge_mixins.py
@@ -1,6 +1,7 @@
from typing import Any
from openslides_backend.services.database.interface import PartialModel
+from openslides_backend.shared.history_events import update_history_information
from ....models.models import (
AssignmentCandidate,
@@ -99,9 +100,11 @@ def get_full_history_information(self) -> HistoryInformation | None:
]:
assignment_ids.add(data["assignment_id"])
for assignment_id in assignment_ids:
- information[fqid_from_collection_and_id("assignment", assignment_id)] = [
- "Candidates merged"
- ]
+ update_history_information(
+ information,
+ fqid_from_collection_and_id("assignment", assignment_id),
+ ["Candidates merged"],
+ )
return information
@@ -128,11 +131,11 @@ def get_full_history_information(self) -> HistoryInformation | None:
]:
motion_ids.add(data["motion_id"])
for motion_id in motion_ids:
- fqid = fqid_from_collection_and_id("motion", motion_id)
- if fqid not in information:
- information[fqid] = ["Submitters merged"]
- else:
- information[fqid].append("Submitters merged")
+ update_history_information(
+ information,
+ fqid_from_collection_and_id("motion", motion_id),
+ ["Submitters merged"],
+ )
return information
@@ -155,11 +158,11 @@ def get_full_history_information(self) -> HistoryInformation | None:
]:
motion_ids.add(data["motion_id"])
for motion_id in motion_ids:
- fqid = fqid_from_collection_and_id("motion", motion_id)
- if fqid not in information:
- information[fqid] = ["Supporters merged"]
- else:
- information[fqid].append("Supporters merged")
+ update_history_information(
+ information,
+ fqid_from_collection_and_id("motion", motion_id),
+ ["Supporters merged"],
+ )
return information
diff --git a/openslides_backend/action/actions/user/merge_together.py b/openslides_backend/action/actions/user/merge_together.py
index 5f690b0235..2dbe1e5e4f 100644
--- a/openslides_backend/action/actions/user/merge_together.py
+++ b/openslides_backend/action/actions/user/merge_together.py
@@ -1,6 +1,10 @@
from typing import Any
from openslides_backend.services.database.interface import PartialModel
+from openslides_backend.shared.history_events import (
+ update_history_information,
+ update_history_information_multi,
+)
from ....action.mixins.archived_meeting_check_mixin import CheckForArchivedMeetingMixin
from ....models.models import User
@@ -449,12 +453,17 @@ def get_full_history_information(self) -> HistoryInformation | None:
deleted_string = " and ".join(
["{}" for i in range(len(deleted_fqids))]
)
- information[main_fqid] = [
- "Updated with data from " + deleted_string,
- *deleted_fqids,
- ]
- for deleted_fqid in deleted_fqids:
- information[deleted_fqid] = ["Merged into {}", main_fqid]
+ update_history_information(
+ information,
+ main_fqid,
+ [
+ "Updated with data from " + deleted_string,
+ *deleted_fqids,
+ ],
+ )
+ update_history_information_multi(
+ information, deleted_fqids, ["Merged into {}", main_fqid]
+ )
else:
raise BadCodingException("No id found for user history generation")
return information
diff --git a/openslides_backend/action/actions/user/set_present.py b/openslides_backend/action/actions/user/set_present.py
index d9ea8650bb..33473fa54c 100644
--- a/openslides_backend/action/actions/user/set_present.py
+++ b/openslides_backend/action/actions/user/set_present.py
@@ -1,5 +1,6 @@
from typing import Any
+from openslides_backend.shared.history_events import build_history_information_data
from openslides_backend.shared.typing import HistoryInformation
from ....action.mixins.archived_meeting_check_mixin import CheckForArchivedMeetingMixin
@@ -40,6 +41,7 @@ def get_updated_instances(self, action_data: ActionData) -> ActionData:
add meeting_id if present is True.
remove meeting_id if present is False.
"""
+ self.base_history_information = {}
for instance in action_data:
meeting_id = instance.pop("meeting_id")
present = instance.pop("present")
@@ -47,17 +49,28 @@ def get_updated_instances(self, action_data: ActionData) -> ActionData:
fqid_from_collection_and_id(self.model.collection, instance["id"]),
["is_present_in_meeting_ids"],
)
+ self.base_history_information[instance["id"]] = {
+ "present": present,
+ "meeting_id": meeting_id,
+ }
if present:
if meeting_id not in user.get("is_present_in_meeting_ids", []):
- instance["is_present_in_meeting_ids"] = user.get(
- "is_present_in_meeting_ids", []
- ) + [meeting_id]
+ is_present = user.get("is_present_in_meeting_ids", []) + [
+ meeting_id
+ ]
+ instance["is_present_in_meeting_ids"] = is_present
+ self.base_history_information[instance["id"]][
+ "is_present_in_meeting_ids"
+ ] = is_present
yield instance
elif present is False:
is_present = user.get("is_present_in_meeting_ids", [])
if meeting_id in is_present:
is_present.remove(meeting_id)
instance["is_present_in_meeting_ids"] = is_present
+ self.base_history_information[instance["id"]][
+ "is_present_in_meeting_ids"
+ ] = is_present
yield instance
def check_permissions(self, instance: dict[str, Any]) -> None:
@@ -94,9 +107,14 @@ def check_permissions(self, instance: dict[str, Any]) -> None:
def get_history_information(self) -> HistoryInformation | None:
return {
- fqid_from_collection_and_id(self.model.collection, instance["id"]): [
- f"Set {'not ' if not instance['present'] else ''}present in meeting {{}}",
- fqid_from_collection_and_id("meeting", instance["meeting_id"]),
- ]
- for instance in self.action_data
+ fqid_from_collection_and_id(
+ self.model.collection, id_
+ ): build_history_information_data(
+ [
+ f"Set {'not ' if not data['present'] else ''}present in meeting {{}}",
+ fqid_from_collection_and_id("meeting", data["meeting_id"]),
+ ],
+ {"is_present_in_meeting_ids": data["is_present_in_meeting_ids"]},
+ )
+ for id_, data in self.base_history_information.items()
}
diff --git a/openslides_backend/action/actions/user/user_mixins.py b/openslides_backend/action/actions/user/user_mixins.py
index 5d0b652383..68870246c4 100644
--- a/openslides_backend/action/actions/user/user_mixins.py
+++ b/openslides_backend/action/actions/user/user_mixins.py
@@ -4,6 +4,7 @@
from openslides_backend.services.database.commands import GetManyRequest
from openslides_backend.services.database.interface import PartialModel
+from openslides_backend.shared.history_events import update_history_information
from openslides_backend.shared.typing import HistoryInformation
from openslides_backend.shared.util import ONE_ORGANIZATION_FQID
@@ -154,12 +155,12 @@ def meeting_user_set_data(self, instance: dict[str, Any]) -> None:
class UpdateHistoryMixin(Action):
def get_history_information(self) -> HistoryInformation | None:
- information = {}
+ information: HistoryInformation = {}
# Scan the instances and collect the info for the history information
# Copy instances first since they are modified
for instance in deepcopy(self.instances):
- instance_information = []
+ entries = []
# Fetch the current instance from the db to diff with the given instance
db_instance = self.datastore.get(
@@ -190,22 +191,24 @@ def get_history_information(self) -> HistoryInformation | None:
"default_vote_weight",
]
if any(field in instance for field in update_fields):
- instance_information.append("Personal data changed")
+ entries.append("Personal data changed")
# other fields
if "organization_management_level" in instance:
- instance_information.append("Organization Management Level changed")
+ entries.append("Organization Management Level changed")
if "committee_management_ids" in instance:
- instance_information.append("Committee management changed")
+ entries.append("Committee management changed")
if "is_active" in instance:
if instance["is_active"]:
- instance_information.append("Set active")
+ entries.append("Set active")
else:
- instance_information.append("Set inactive")
+ entries.append("Set inactive")
- if instance_information:
- information[fqid_from_collection_and_id("user", instance["id"])] = (
- instance_information
+ if entries:
+ update_history_information(
+ information,
+ fqid_from_collection_and_id("user", instance["id"]),
+ entries,
)
return information
diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py
index bbcfe6c07b..5cf7155e92 100644
--- a/openslides_backend/models/models.py
+++ b/openslides_backend/models/models.py
@@ -535,6 +535,7 @@ class HistoryEntry(Model):
id = fields.IntegerField(required=True, constant=True)
entries = fields.TextArrayField()
+ changed_fields = fields.JSONField()
original_model_id = fields.CharField(constant=True)
model_id = fields.GenericRelationField(
to={
diff --git a/openslides_backend/shared/history_events.py b/openslides_backend/shared/history_events.py
index 1f0eaef75c..0b980d210a 100644
--- a/openslides_backend/shared/history_events.py
+++ b/openslides_backend/shared/history_events.py
@@ -2,13 +2,58 @@
from typing import Any
from zoneinfo import ZoneInfo
+from psycopg.types.json import Jsonb
+
from .interfaces.event import ListFields
from .patterns import FullQualifiedId, fqid_from_collection_and_id
-from .typing import HistoryInformation
+from .typing import HistoryInformation, HistoryInformationData
EventPayload = tuple[FullQualifiedId, dict[str, Any] | ListFields]
+def build_history_information_data(
+ entries: list[str] | None = None,
+ changed_fields: dict[str, Any] | None = None,
+) -> HistoryInformationData:
+ data: HistoryInformationData = {}
+ if entries is not None:
+ data["entries"] = entries
+ if changed_fields is not None:
+ data["changed_fields"] = changed_fields
+ return data
+
+
+def update_history_information(
+ information: HistoryInformation,
+ fqid: FullQualifiedId,
+ entries: list[str] | None = None,
+ changed_fields: dict[str, Any] | None = None,
+) -> None:
+ """Updates history information for fqid"""
+ if fqid not in information:
+ information[fqid] = build_history_information_data(entries, changed_fields)
+ else:
+ if entries:
+ information[fqid].setdefault("entries", list()).extend(entries)
+ if changed_fields:
+ information[fqid].setdefault("changed_fields", dict()).update(
+ changed_fields
+ )
+
+
+def update_history_information_multi(
+ information: HistoryInformation,
+ fqids: list[FullQualifiedId],
+ entries: list[str] | None = None,
+ changed_fields: dict[str, Any] | None = None,
+) -> None:
+ """
+ Adds given HistoryInformation to the given information for every fqid in fqids.
+ """
+ for fqid in fqids:
+ update_history_information(information, fqid, entries, changed_fields)
+
+
def calculate_history_event_payloads(
user_id: int | None,
information: HistoryInformation,
@@ -19,8 +64,13 @@ def calculate_history_event_payloads(
timestamp: int | None = None,
) -> list[EventPayload]:
transformed_information = [
- (model_fqid_to_entry_id[fqid], fqid, entries)
- for fqid, entries in information.items()
+ (
+ model_fqid_to_entry_id[fqid],
+ fqid,
+ data["entries"],
+ data.get("changed_fields"),
+ )
+ for fqid, data in information.items()
]
create_events: list[EventPayload] = [
(
@@ -28,13 +78,14 @@ def calculate_history_event_payloads(
{
"id": id_,
"entries": entries,
+ "changed_fields": Jsonb(changed_fields) if changed_fields else None,
"position_id": position_id,
"original_model_id": fqid,
"model_id": (fqid if fqid in existing_fqids else None),
"meeting_id": model_fqid_to_meeting_id.get(fqid, None),
},
)
- for id_, fqid, entries in transformed_information
+ for id_, fqid, entries, changed_fields in transformed_information
]
create_events.append(
(
diff --git a/openslides_backend/shared/patterns.py b/openslides_backend/shared/patterns.py
index 3519e27cd3..6687622ede 100644
--- a/openslides_backend/shared/patterns.py
+++ b/openslides_backend/shared/patterns.py
@@ -137,7 +137,7 @@ def collection_and_id_from_fqid(fqid: str) -> tuple[str, int]:
# Build FQIDs
-def fqid_from_collection_and_id(collection: str, id: str | int) -> str:
+def fqid_from_collection_and_id(collection: str, id: str | int) -> FullQualifiedId:
return f"{collection}{KEYSEPARATOR}{id}"
diff --git a/openslides_backend/shared/typing.py b/openslides_backend/shared/typing.py
index fbba186e7b..e0a2d531f6 100644
--- a/openslides_backend/shared/typing.py
+++ b/openslides_backend/shared/typing.py
@@ -1,6 +1,6 @@
-from typing import Any, Union
+from typing import Any, NotRequired, TypedDict, Union
-from .patterns import Collection, Id
+from .patterns import Collection, Field, FullQualifiedId, Id
PartialModel = dict[str, Any]
Model = dict[str, Any]
@@ -8,7 +8,13 @@
Schema = dict[str, Any]
-HistoryInformation = dict[str, list[str]]
+
+class HistoryInformationData(TypedDict):
+ entries: NotRequired[list[str]]
+ changed_fields: NotRequired[dict[Field, Any]]
+
+
+HistoryInformation = dict[FullQualifiedId, HistoryInformationData]
JSON = Union[str, int, float, bool, None, dict[str, Any], list[Any]]
diff --git a/tests/database/reader/system/test_filter.py b/tests/database/reader/system/test_filter.py
index 7f83e16244..42c6bdea42 100644
--- a/tests/database/reader/system/test_filter.py
+++ b/tests/database/reader/system/test_filter.py
@@ -176,6 +176,7 @@ def test_types_str_list(db_connection: Connection) -> None:
1: {
"id": 1,
"entries": ["User added to meetings"],
+ "changed_fields": None,
"meeting_id": 1,
"model_id": "user/1",
"model_id_assignment_id": None,
diff --git a/tests/system/action/base.py b/tests/system/action/base.py
index 6a138b3a1d..a8a3bc475c 100644
--- a/tests/system/action/base.py
+++ b/tests/system/action/base.py
@@ -15,6 +15,7 @@
from openslides_backend.permissions.management_levels import OrganizationManagementLevel
from openslides_backend.permissions.permissions import Permission
from openslides_backend.services.database.commands import GetManyRequest
+from openslides_backend.services.database.interface import PartialModel
from openslides_backend.shared.exceptions import AuthenticationException
from openslides_backend.shared.filters import FilterOperator
from openslides_backend.shared.patterns import (
@@ -297,7 +298,9 @@ def base_locked_out_superadmin_permission_test(
True,
)
- def get_last_history_information(self, fqid: FullQualifiedId) -> list[str] | None:
+ def get_last_history_information(
+ self, fqid: FullQualifiedId
+ ) -> PartialModel | None:
entry_id = self.datastore.max(
"history_entry",
FilterOperator("original_model_id", "=", fqid),
@@ -305,37 +308,44 @@ def get_last_history_information(self, fqid: FullQualifiedId) -> list[str] | Non
lock_result=False,
)
if entry_id:
- history_entry = self.datastore.get(
+ return self.datastore.get(
fqid_from_collection_and_id("history_entry", entry_id),
- ["entries"],
+ ["entries", "changed_fields"],
lock_result=False,
)
- return history_entry.get("entries")
else:
return None
def assert_history_information(
- self, fqid: FullQualifiedId, information: list[str] | None
+ self,
+ fqid: FullQualifiedId,
+ entries: list[str] | None,
+ changed_fields: dict[str, Any] | None = None,
) -> None:
"""
Asserts that the last history information for the given model is the given information.
"""
last_information = self.get_last_history_information(fqid)
- if information is None:
- assert not last_information
+ if entries is None and changed_fields is None:
+ assert (
+ not last_information
+ ), f"Expected no history information to be generated for {fqid}. Got:\n{last_information}"
else:
- assert last_information
- self.assertEqual(last_information, information)
+ assert (
+ last_information
+ ), f"No history information was be generated for {fqid}."
+ self.assertEqual(last_information.get("entries"), entries)
+ self.assertEqual(last_information.get("changed_fields"), changed_fields)
def assert_history_information_contains(
- self, fqid: FullQualifiedId, information: str
+ self, fqid: FullQualifiedId, entry: str
) -> None:
"""
Asserts that the last history information for the given model is the given information.
"""
last_information = self.get_last_history_information(fqid)
- assert last_information
- assert information in last_information
+ assert last_information, f"No history information was be generated for {fqid}."
+ self.assertIn(entry, last_information["entries"])
def assert_logged_in(self) -> None:
self.auth.authenticate() # assert that no exception is thrown
diff --git a/tests/system/action/user/test_create.py b/tests/system/action/user/test_create.py
index 1a3040ef3b..a4208090f9 100644
--- a/tests/system/action/user/test_create.py
+++ b/tests/system/action/user/test_create.py
@@ -133,6 +133,7 @@ def test_create_some_more_fields(self) -> None:
"group/114",
"meeting/114",
],
+ {"group_ids": [114]},
)
def test_create_comment(self) -> None:
diff --git a/tests/system/action/user/test_delegation_history.py b/tests/system/action/user/test_delegation_history.py
index df5ce87bc3..b491b0f988 100644
--- a/tests/system/action/user/test_delegation_history.py
+++ b/tests/system/action/user/test_delegation_history.py
@@ -44,6 +44,8 @@ def assert_delegated_to(
to_id: int,
prepend_to: list[str] = [],
prepend_from: list[str] = [],
+ changed_fields_to: dict[str, list[int]] | None = None,
+ changed_fields_from: dict[str, list[int]] | None = None,
) -> None:
self.assert_history_information(
f"user/{to_id}",
@@ -53,6 +55,7 @@ def assert_delegated_to(
f"user/{from_id}",
"meeting/1",
],
+ changed_fields_to,
)
self.assert_history_information(
f"user/{from_id}",
@@ -62,9 +65,15 @@ def assert_delegated_to(
f"user/{to_id}",
"meeting/1",
],
+ changed_fields_from,
)
- def assert_alice_redelegated_to(self, who_id: int, prepend: list[str] = []) -> None:
+ def assert_alice_redelegated_to(
+ self,
+ who_id: int,
+ prepend: list[str] = [],
+ changed_fields: dict[str, list[int]] | None = None,
+ ) -> None:
self.assert_history_information(
f"user/{who_id}",
[
@@ -73,6 +82,7 @@ def assert_alice_redelegated_to(self, who_id: int, prepend: list[str] = []) -> N
f"user/{self.alice_id}",
"meeting/1",
],
+ changed_fields,
)
self.assert_history_information(
f"user/{self.alice_id}",
@@ -116,6 +126,7 @@ def test_create_delegate_vote(self) -> None:
"group/3",
"meeting/1",
],
+ changed_fields_from={"group_ids": [3]},
)
def test_create_receive_delegated_vote(self) -> None:
@@ -133,6 +144,7 @@ def test_create_receive_delegated_vote(self) -> None:
"group/3",
"meeting/1",
],
+ changed_fields_to={"group_ids": [3]},
)
def test_update_re_delegate_vote(self) -> None:
@@ -162,6 +174,7 @@ def test_create_re_delegate_vote_reverse(self) -> None:
"group/3",
"meeting/1",
],
+ changed_fields={"group_ids": [3]},
)
def test_update_re_delegate_received_votes(self) -> None:
@@ -371,6 +384,7 @@ def test_create_multiple_from_ids(self) -> None:
],
"meeting/1",
],
+ {"group_ids": [3]},
)
for id_ in [self.alice_id, self.bob_id, self.colin_id, eric_id, fredric_id]:
self.assert_history_information(
@@ -404,6 +418,7 @@ def test_update_create_meeting_user_receiving_delegation(self) -> None:
f"user/{self.alice_id}",
"meeting/1",
],
+ {"group_ids": [3]},
)
def test_update_create_meeting_user_with_delegation(self) -> None:
@@ -431,4 +446,5 @@ def test_update_create_meeting_user_with_delegation(self) -> None:
f"user/{self.alice_id}",
"meeting/1",
],
+ {"group_ids": [3]},
)
diff --git a/tests/system/action/user/test_set_present.py b/tests/system/action/user/test_set_present.py
index 17b759b660..da18e3f5d0 100644
--- a/tests/system/action/user/test_set_present.py
+++ b/tests/system/action/user/test_set_present.py
@@ -18,23 +18,40 @@ def test_set_present_add_correct(self) -> None:
"user.set_present", {"id": 111, "meeting_id": 1, "present": True}
)
self.assert_status_code(response, 200)
- model = self.get_model("user/111")
- assert model.get("is_present_in_meeting_ids") == [1]
- meeting = self.get_model("meeting/1")
- assert meeting.get("present_user_ids") == [111]
+ self.assert_model_exists("user/111", {"is_present_in_meeting_ids": [1]})
+ self.assert_model_exists("meeting/1", {"present_user_ids": [111]})
self.assert_history_information(
- "user/111", ["Set present in meeting {}", "meeting/1"]
+ "user/111",
+ ["Set present in meeting {}", "meeting/1"],
+ {"is_present_in_meeting_ids": [1]},
+ )
+
+ def test_set_present_add_second_correct(self) -> None:
+ self.set_models(
+ {
+ "meeting/1": {"present_user_ids": [111]},
+ "user/111": {"username": "username_srtgb123"},
+ }
+ )
+ self.create_meeting(4)
+ response = self.request(
+ "user.set_present", {"id": 111, "meeting_id": 4, "present": True}
+ )
+ self.assert_status_code(response, 200)
+ self.assert_model_exists("user/111", {"is_present_in_meeting_ids": [1, 4]})
+ self.assert_model_exists("meeting/1", {"present_user_ids": [111]})
+ self.assert_model_exists("meeting/4", {"present_user_ids": [111]})
+ self.assert_history_information(
+ "user/111",
+ ["Set present in meeting {}", "meeting/4"],
+ {"is_present_in_meeting_ids": [1, 4]},
)
def test_set_present_del_correct(self) -> None:
self.set_models(
{
- "meeting/1": {
- "present_user_ids": [111],
- },
- "user/111": {
- "username": "username_srtgb123",
- },
+ "meeting/1": {"present_user_ids": [111]},
+ "user/111": {"username": "username_srtgb123"},
}
)
response = self.request(
@@ -44,7 +61,9 @@ def test_set_present_del_correct(self) -> None:
self.assert_model_exists("user/111", {"is_present_in_meeting_ids": None})
self.assert_model_exists("meeting/1", {"present_user_ids": None})
self.assert_history_information(
- "user/111", ["Set not present in meeting {}", "meeting/1"]
+ "user/111",
+ ["Set not present in meeting {}", "meeting/1"],
+ {"is_present_in_meeting_ids": []},
)
def test_set_present_null_action(self) -> None:
diff --git a/tests/system/action/user/test_update.py b/tests/system/action/user/test_update.py
index faa8b64173..1299dd7480 100644
--- a/tests/system/action/user/test_update.py
+++ b/tests/system/action/user/test_update.py
@@ -289,6 +289,7 @@ def test_update_with_meeting_user_fields(self) -> None:
"meeting/1",
"Committee management changed",
],
+ {"group_ids": [1]},
)
self.assert_history_information(
"user/23", ["Vote delegated to {} in meeting {}", "user/22", "meeting/1"]
@@ -490,6 +491,7 @@ def test_committee_manager_without_committee_ids(self) -> None:
"Personal data changed",
"Committee management changed",
],
+ {"group_ids": []},
)
def test_committee_manager_remove_committee_ids(self) -> None:
@@ -2680,6 +2682,7 @@ def test_update_history_add_group(self) -> None:
self.assert_history_information(
f"user/{user_id}",
["Participant added to group {} in meeting {}", "group/3", "meeting/1"],
+ {"group_ids": [2, 3, 10, 11, 12]},
)
def test_update_history_add_group_to_default_group(self) -> None:
@@ -2700,6 +2703,7 @@ def test_update_history_add_group_to_default_group(self) -> None:
self.assert_history_information(
f"user/{user_id}",
["Participant added to group {} in meeting {}", "group/2", "meeting/1"],
+ {"group_ids": [2, 10, 11, 12]},
)
def test_update_history_add_multiple_groups(self) -> None:
@@ -2720,6 +2724,7 @@ def test_update_history_add_multiple_groups(self) -> None:
self.assert_history_information(
f"user/{user_id}",
["Participant added to multiple groups in meeting {}", "meeting/1"],
+ {"group_ids": [2, 3, 10, 11, 12]},
)
def test_update_history_add_multiple_groups_with_default_group(self) -> None:
@@ -2739,6 +2744,7 @@ def test_update_history_add_multiple_groups_with_default_group(self) -> None:
self.assert_history_information(
f"user/{user_id}",
["Participant added to group {} in meeting {}", "group/2", "meeting/1"],
+ {"group_ids": [1, 2]},
)
def test_update_history_remove_group(self) -> None:
@@ -2763,6 +2769,7 @@ def test_update_history_remove_group(self) -> None:
self.assert_history_information(
f"user/{user_id}",
["Participant removed from meeting {}", "meeting/1"],
+ {"group_ids": []},
)
def test_update_fields_with_equal_value_no_history(self) -> None:
@@ -2859,6 +2866,7 @@ def test_update_participant_data_with_existing_meetings(self) -> None:
"group/4",
"meeting/4",
],
+ {"group_ids": [1, 4]},
)
def test_update_participant_data_in_multiple_meetings_with_existing_meetings(
@@ -2908,6 +2916,7 @@ def test_update_participant_data_in_multiple_meetings_with_existing_meetings(
"group/7",
"meeting/7",
],
+ {"group_ids": [1, 4, 7]},
)
def test_update_saml_id__can_change_own_password_error(self) -> None:
From b8ba5b909cf88d5abb180bd4e1892008cb903ee2 Mon Sep 17 00:00:00 2001
From: Viktoriia Krasnovyd <114735598+vkrasnovyd@users.noreply.github.com>
Date: Mon, 13 Jul 2026 11:57:57 +0200
Subject: [PATCH 18/23] [vote] Update meeting defaults for polls (#3644)
(#3653)
---
cli/generate_models.py | 2 +
data/example-data.json | 92 ++++----
docs/Actions-Overview.md | 8 +
docs/actions/meeting.create.md | 19 +-
docs/actions/meeting.update.md | 44 ++--
docs/actions/meeting_poll_default.create.md | 43 ++++
docs/actions/meeting_poll_default.delete.md | 8 +
docs/actions/meeting_poll_default.update.md | 22 ++
docs/actions/projector.create.md | 2 +-
docs/actions/projector.update.md | 2 +-
meta | 2 +-
openslides_backend/action/actions/__init__.py | 1 +
.../action/actions/meeting/create.py | 19 +-
.../action/actions/meeting/update.py | 77 +++++--
.../actions/meeting_poll_default/__init__.py | 1 +
.../actions/meeting_poll_default/create.py | 118 ++++++++++
.../actions/meeting_poll_default/delete.py | 15 ++
.../meeting_poll_default/helper_mixin.py | 16 ++
.../actions/meeting_poll_default/update.py | 26 +++
openslides_backend/models/mixins.py | 10 +-
openslides_backend/models/models.py | 195 ++++++++---------
tests/database/reader/system/test_filter.py | 5 +-
tests/database/reader/system/util.py | 12 -
tests/database/writer/system/test_delete.py | 4 +-
tests/system/action/meeting/test_clone.py | 93 ++++++--
tests/system/action/meeting/test_create.py | 75 ++++++-
tests/system/action/meeting/test_delete.py | 2 +
tests/system/action/meeting/test_import.py | 104 +++++----
tests/system/action/meeting/test_settings.py | 127 ++++++++++-
tests/system/action/meeting/test_update.py | 17 +-
.../action/meeting_poll_default/__init__.py | 0
.../meeting_poll_default/test_create.py | 206 ++++++++++++++++++
.../meeting_poll_default/test_update.py | 77 +++++++
tests/system/presenter/test_check_database.py | 52 +++--
.../presenter/test_check_database_all.py | 52 +++--
tests/system/presenter/test_export_meeting.py | 84 +++++--
tests/unit/test_checker.py | 4 +-
37 files changed, 1298 insertions(+), 338 deletions(-)
create mode 100644 docs/actions/meeting_poll_default.create.md
create mode 100644 docs/actions/meeting_poll_default.delete.md
create mode 100644 docs/actions/meeting_poll_default.update.md
create mode 100644 openslides_backend/action/actions/meeting_poll_default/__init__.py
create mode 100644 openslides_backend/action/actions/meeting_poll_default/create.py
create mode 100644 openslides_backend/action/actions/meeting_poll_default/delete.py
create mode 100644 openslides_backend/action/actions/meeting_poll_default/helper_mixin.py
create mode 100644 openslides_backend/action/actions/meeting_poll_default/update.py
create mode 100644 tests/system/action/meeting_poll_default/__init__.py
create mode 100644 tests/system/action/meeting_poll_default/test_create.py
create mode 100644 tests/system/action/meeting_poll_default/test_update.py
diff --git a/cli/generate_models.py b/cli/generate_models.py
index 018b09d8ca..5e5b0d1d2f 100644
--- a/cli/generate_models.py
+++ b/cli/generate_models.py
@@ -18,6 +18,7 @@
from openslides_backend.models.mixins import (
AgendaItemModelMixin,
MeetingModelMixin,
+ MeetingPollDefaultModelMixin,
PollModelMixin,
)
from openslides_backend.shared.patterns import KEYSEPARATOR, Collection
@@ -59,6 +60,7 @@
MODEL_MIXINS: dict[str, type] = {
"agenda_item": AgendaItemModelMixin,
"meeting": MeetingModelMixin,
+ "meeting_poll_default": MeetingPollDefaultModelMixin,
"poll": PollModelMixin,
}
diff --git a/data/example-data.json b/data/example-data.json
index 8403ecda8e..22d1e4cc14 100644
--- a/data/example-data.json
+++ b/data/example-data.json
@@ -354,17 +354,8 @@
"motions_export_submitter_recommendation": true,
"motions_export_follow_recommendation": false,
"motions_enable_restricted_editor_for_non_manager": true,
- "motion_poll_ballot_paper_selection": "CUSTOM_NUMBER",
- "motion_poll_ballot_paper_number": 8,
- "motion_poll_default_type": "secret",
- "motion_poll_default_allow_abstain": true,
- "motion_poll_default_onehundred_percent_base": "valid",
- "motion_poll_default_group_ids": [
- 2,
- 3
- ],
- "motion_poll_projection_name_order_first": "last_name",
- "motion_poll_projection_max_columns": 6,
+ "poll_projection_name_order_first": "last_name",
+ "poll_projection_max_columns": 6,
"users_enable_presence_view": true,
"users_enable_vote_weight": false,
"users_enable_vote_delegations": true,
@@ -376,27 +367,13 @@
"users_email_subject": "OpenSlides access data",
"users_email_body": "Dear {name},\n\nthis is your personal OpenSlides login:\n\n{url}\nUsername: {username}\nPassword: {password}\n\n\nThis email was generated automatically.",
"assignments_export_title": "Elections",
- "assignment_poll_ballot_paper_selection": "CUSTOM_NUMBER",
- "assignment_poll_ballot_paper_number": 8,
"assignment_poll_add_candidates_to_list_of_speakers": false,
- "assignment_poll_enable_max_votes_per_option": false,
- "assignment_poll_sort_poll_result_by_votes": true,
- "assignment_poll_default_type": "secret",
+ "poll_enable_max_votes_per_option": false,
"assignment_poll_default_method": "selection",
- "assignment_poll_default_onehundred_percent_base": "valid",
- "assignment_poll_default_group_ids": [
- 2,
- 3
- ],
- "poll_ballot_paper_selection": "CUSTOM_NUMBER",
- "poll_ballot_paper_number": 8,
- "poll_sort_poll_result_by_votes": true,
- "poll_default_type": "nominal",
- "poll_default_method": "votes",
- "poll_default_onehundred_percent_base": "valid",
- "poll_default_group_ids": [
- 3
- ],
+ "assignment_poll_config_id": 1,
+ "motion_poll_config_id": 2,
+ "topic_poll_config_id": 3,
+ "topic_poll_default_method": "selection",
"poll_default_live_voting_enabled": false,
"poll_default_allow_invalid": false,
"poll_default_allow_vote_split": false,
@@ -545,6 +522,7 @@
4,
5
],
+ "poll_default_ids": [1, 2, 3],
"poll_ids": [
1,
2,
@@ -600,7 +578,7 @@
"default_projector_countdown_ids": [1],
"default_projector_assignment_poll_ids": [1],
"default_projector_motion_poll_ids": [1],
- "default_projector_poll_ids": [1],
+ "default_projector_topic_poll_ids": [1],
"projection_ids": [
3
],
@@ -609,6 +587,45 @@
"meeting_mediafile_ids": [1]
}
},
+ "meeting_poll_default": {
+ "1": {
+ "id": 1,
+ "meeting_id": 1,
+ "used_as_assignment_poll_config_in_meeting_id": 1,
+ "group_ids": [2, 3],
+ "sort_result_by_votes": true,
+ "visibility": "secret",
+ "onehundred_percent_base": "valid",
+ "allow_abstain": false,
+ "allow_nota": false,
+ "strike_out": false
+ },
+ "2": {
+ "id": 2,
+ "meeting_id": 1,
+ "used_as_motion_poll_config_in_meeting_id": 1,
+ "group_ids": [2, 3],
+ "sort_result_by_votes": true,
+ "visibility": "secret",
+ "onehundred_percent_base": "valid",
+ "allow_abstain": true,
+ "allow_nota": false,
+ "strike_out": false
+ },
+ "3": {
+ "id": 3,
+ "meeting_id": 1,
+ "used_as_topic_poll_config_in_meeting_id": 1,
+ "group_ids": [3],
+ "sort_result_by_votes": true,
+ "visibility": "named",
+ "onehundred_percent_base": "valid",
+ "allow_abstain": true,
+ "allow_nota": false,
+ "strike_out": false,
+ "display_chart": "pie"
+ }
+ },
"group": {
"1": {
"id": 1,
@@ -658,9 +675,8 @@
"poll_ids": [
5
],
- "used_as_motion_poll_default_id": 1,
- "used_as_assignment_poll_default_id": 1,
- "meeting_id": 1
+ "meeting_id": 1,
+ "used_in_meeting_poll_default_ids": [1, 2]
},
"3": {
"id": 3,
@@ -698,10 +714,8 @@
"write_comment_section_ids": [
1
],
- "used_as_motion_poll_default_id": 1,
- "used_as_assignment_poll_default_id": 1,
- "used_as_poll_default_id": 1,
- "meeting_id": 1
+ "meeting_id": 1,
+ "used_in_meeting_poll_default_ids": [1, 2, 3]
},
"4": {
"id": 4,
@@ -2297,7 +2311,7 @@
"used_as_default_projector_for_countdown_in_meeting_id": 1,
"used_as_default_projector_for_assignment_poll_in_meeting_id": 1,
"used_as_default_projector_for_motion_poll_in_meeting_id": 1,
- "used_as_default_projector_for_poll_in_meeting_id": 1,
+ "used_as_default_projector_for_topic_poll_in_meeting_id": 1,
"meeting_id": 1
},
"2": {
diff --git a/docs/Actions-Overview.md b/docs/Actions-Overview.md
index 0a30df6fc6..605d052f1f 100644
--- a/docs/Actions-Overview.md
+++ b/docs/Actions-Overview.md
@@ -78,6 +78,12 @@ A more general format description see in [Action-Service](https://github.com/Ope
- [meeting.archive](actions/meeting.archive.md)
- [meeting.unarchive](actions/meeting.unarchive.md)
+## Meeting poll defaults
+
+- [meeting_poll_default.create](actions/meeting_poll_default.create.md)
+- [meeting_poll_default.update](actions/meeting_poll_default.delete.md)
+- [meeting_poll_default.update](actions/meeting_poll_default.update.md)
+
## Motions
- [motion.create](actions/motion.create.md)
@@ -142,6 +148,7 @@ A more general format description see in [Action-Service](https://github.com/Ope
- [personal_note.update](actions/personal_note.update.md)
## Projector and Projection
+
- [projector.create](actions/projector.create.md)
- [projector.update](actions/projector.update.md)
- [projector.delete](actions/projector.delete.md)
@@ -219,6 +226,7 @@ A more general format description see in [Action-Service](https://github.com/Ope
- [topic.import](actions/topic.import.md)
## Users
+
- [user.assign_meetings](actions/user.assign_meetings.md)
- [user.create](actions/user.create.md)
- [user.delete](actions/user.delete.md)
diff --git a/docs/actions/meeting.create.md b/docs/actions/meeting.create.md
index ef701df30f..cab505f6a8 100644
--- a/docs/actions/meeting.create.md
+++ b/docs/actions/meeting.create.md
@@ -24,15 +24,16 @@
Creates a meeting.
Checks whether the `organization.limit_of_meetings` is unlimited (=0) or lower than the amount of active meetings in `organization.active_meeting_ids`.
-
+
`time_zone` must be a valid timezone string (see [presenter](../presenters/get_valid_timezones.md)).
The following objects are created, too:
-- Groups: `Default`, `Admin`, `Delegates`, `Staff`, `Committees`. The first one is set as `meeting/default_group_id`, the second one as `meeting/admin_group_id`. The permissions can be found in the [initial-data.json](https://github.com/OpenSlides/openslides-backend/tree/main/data/initial-data.json)).
+- Groups: `Default`, `Admin`, `Delegates`, `Staff`, `Committees`. The first one is set as `meeting/default_group_id`, the second one as `meeting/admin_group_id`. The permissions can be found in the [initial-data.json](https://github.com/OpenSlides/openslides-backend/tree/main/data/initial-data.json).
- Projector: One projector named `"Default projector"` is created and set as `meeting/reference_projector_id`.
- All default projectors (`meeting/default_projector_*_ids`, see `models.yml`) are set to that one projector
- Motion workflow and states: Creates one workflow `"simple workflow"` which is set as `meeting/motions_default_workflow_id` and `meeting/motions_default_amendment_workflow_id`. Creates four states (analog as in the [initial-data.json](https://github.com/OpenSlides/openslides-backend/tree/main/data/initial-data.json)).
- Two countdowns are created and set as `meeting/list_of_speakers_countdown` (name: "List of speakers countdown") and `meeting/voting_countdown` (name: "Voting countdown").
+- 3 `meeting_poll_default` items: one for each poll type (assignment, motion, topic).
If `user_ids` are given, it checks if it is a subset of `committee/user_ids`. Each user is added to the meeting by being added to the default group.
@@ -41,15 +42,17 @@ If they aren't given and `set_as_template` is not true, there will be an error.
The field `is_active_in_organization_id` is set to the organization_id.
-If a meeting is created,
-* `motion_poll_default_type` is `pseudoanonymous`
-* `assignment_poll_default_type` is `pseudoanonymous`
-* `assignment_poll_default_method` is `Y`
-* `assignment_poll_default_group_ids`, `motion_poll_default_group_ids` and `topic_poll_default_group_ids` have the Delegates group.
+If a meeting is created, defaults are set for `meeting_poll_default`:
+* `group_ids` is the Delegates group
+* for topic polls:
+ * `visibility` is `manually`
+ * `display_chart` is `pie`
+* for motion and topic polls:
+ * `visibility` is `secret`
If `set_as_template` is given, `template_for_organization_id` is set to `1`.
-Created objects (eg. groups, workflow, states, ...) are translated to the given language `language`.
+Created objects (eg. groups, workflow, states, ...) are translated to the given language `language`.
## Permissions
The user must have CML `can_manage`.
diff --git a/docs/actions/meeting.update.md b/docs/actions/meeting.update.md
index 2b88ab16ac..6330646f62 100644
--- a/docs/actions/meeting.update.md
+++ b/docs/actions/meeting.update.md
@@ -118,13 +118,8 @@
motions_enable_restricted_editor_for_manager: boolean;
motions_enable_restricted_editor_for_non_manager: boolean;
- motion_poll_ballot_paper_selection: string;
- motion_poll_ballot_paper_number: number;
- motion_poll_default_type: string;
- motion_poll_default_onehundred_percent_base: string;
- motion_poll_default_group_ids: Id[];
- motion_poll_projection_name_order_first: string;
- motion_poll_projection_max_columns: number;
+ poll_projection_name_order_first: string;
+ poll_projection_max_columns: number;
users_enable_presence_view: boolean;
users_enable_vote_weight: boolean;
@@ -147,20 +142,41 @@
assignments_export_title: string;
assignments_export_preamble: string;
- assignment_poll_ballot_paper_selection: string;
- assignment_poll_ballot_paper_number: number;
assignment_poll_add_candidates_to_list_of_speakers: boolean;
- assignment_poll_enable_max_votes_per_option: boolean;
- assignment_poll_sort_poll_result_by_votes: boolean;
- assignment_poll_default_type: string;
assignment_poll_default_method: string;
- assignment_poll_default_onehundred_percent_base: string;
+ assignment_poll_default_allow_abstain: boolean;
+ assignment_poll_default_allow_nota: boolean;
+ assignment_poll_default_display_chart: string;
assignment_poll_default_group_ids: Id[];
+ assignment_poll_default_onehundred_percent_base: string;
+ assignment_poll_default_sort_result_by_votes: boolean;
+ assignment_poll_default_strike_out: boolean;
+ assignment_poll_default_visibility: string;
+ motion_poll_default_allow_abstain: boolean;
+ motion_poll_default_allow_nota: boolean;
+ motion_poll_default_display_chart: string;
+ motion_poll_default_group_ids: Id[];
+ motion_poll_default_onehundred_percent_base: string;
+ motion_poll_default_sort_result_by_votes: boolean;
+ motion_poll_default_strike_out: boolean;
+ motion_poll_default_visibility: string;
+
+ topic_poll_default_method: string;
+ topic_poll_default_allow_abstain: boolean;
+ topic_poll_default_allow_nota: boolean;
+ topic_poll_default_display_chart: string;
topic_poll_default_group_ids: Id[];
+ topic_poll_default_onehundred_percent_base: string;
+ topic_poll_default_sort_result_by_votes: boolean;
+ topic_poll_default_strike_out: boolean;
+ topic_poll_default_visibility: string;
+ poll_enable_max_votes_per_option: boolean;
poll_default_live_voting_enabled: boolean;
poll_default_allow_invalid: boolean;
+ poll_default_allow_vote_split: boolean;
+
// Group B
present_user_ids: user/is_present_in_meeting_ids;
@@ -180,7 +196,7 @@
default_projector_countdown_ids: Ids;
default_projector_assignment_poll_ids: Ids;
default_projector_motion_poll_ids: Ids;
- default_projector_poll_ids: Ids;
+ default_projector_topic_poll_ids: Ids;
// Group D
external_id: string; // unique in org
diff --git a/docs/actions/meeting_poll_default.create.md b/docs/actions/meeting_poll_default.create.md
new file mode 100644
index 0000000000..8831a991f9
--- /dev/null
+++ b/docs/actions/meeting_poll_default.create.md
@@ -0,0 +1,43 @@
+## Payload
+```js
+{
+// required
+ meeting_id: Id;
+
+// exactly 1 out of 3 is required
+ used_as_assignment_poll_config_in_meeting_id: Id;
+ used_as_motion_poll_config_in_meeting_id: Id;
+ used_as_topic_poll_config_in_meeting_id: Id;
+
+// optional
+ allow_abstain: boolean;
+ allow_nota: boolean;
+ display_chart: string;
+ group_ids: Id[]
+ onehundred_percent_base: string;
+ sort_result_by_votes: boolean;
+ strike_out: boolean;
+ visibility: string;
+}
+```
+
+## Internal action
+
+The action creates `meeting_poll_default` item for the meeting. Should only be called by meeting.create or meeting.update.
+
+Along with the `meeting_id` field, exactly one out of the 3 field must be defined:
+
+* used_as_assignment_poll_config_in_meeting_id
+* used_as_motion_poll_config_in_meeting_id
+* used_as_topic_poll_config_in_meeting_id
+
+Value in the field used_as_*_poll_config_in_meeting_id must be the same as `meeting_id`.
+
+All ids in `group_ids` must bellong to the meeting defined by `meeting_id`.
+
+This action sets default values based on the poll type for the fields listed below if no value is given for them in the payload:
+
+* display_chart (only if `used_as_topic_poll_config_in_meeting_id` is set) => "pie"
+* visibility:
+ * if `used_as_topic_poll_config_in_meeting_id` is set => "manually"
+ * else => "secret"
diff --git a/docs/actions/meeting_poll_default.delete.md b/docs/actions/meeting_poll_default.delete.md
new file mode 100644
index 0000000000..de99be2e6a
--- /dev/null
+++ b/docs/actions/meeting_poll_default.delete.md
@@ -0,0 +1,8 @@
+## Payload
+```js
+{ id: Id; }
+```
+
+## Internal action
+
+Delete a `meeting_poll_default` item.
diff --git a/docs/actions/meeting_poll_default.update.md b/docs/actions/meeting_poll_default.update.md
new file mode 100644
index 0000000000..585c406eba
--- /dev/null
+++ b/docs/actions/meeting_poll_default.update.md
@@ -0,0 +1,22 @@
+## Payload
+```js
+{
+// optional
+ allow_abstain: boolean;
+ allow_nota: boolean;
+ display_chart: string;
+ group_ids: Id[]
+ onehundred_percent_base: string;
+ sort_result_by_votes: boolean;
+ strike_out: boolean;
+ visibility: string;
+}
+```
+
+## Internal action
+
+The action updates a `meeting_poll_default` item.
+
+Should only be called by meeting.update.
+
+All ids in `group_ids` must bellong to the meeting defined by `meeting_id`.
diff --git a/docs/actions/projector.create.md b/docs/actions/projector.create.md
index 5f6a282b9c..f7e639a54a 100644
--- a/docs/actions/projector.create.md
+++ b/docs/actions/projector.create.md
@@ -36,7 +36,7 @@
used_as_default_projector_for_countdown_in_meeting_id: Id,
used_as_default_projector_for_assignment_poll_in_meeting_id: Id,
used_as_default_projector_for_motion_poll_in_meeting_id: Id,
- used_as_default_projector_for_poll_in_meeting_id: Id,
+ used_as_default_projector_for_topic_poll_in_meeting_id: Id,
}
```
diff --git a/docs/actions/projector.update.md b/docs/actions/projector.update.md
index 3f6a1dfb44..8e29556f2f 100644
--- a/docs/actions/projector.update.md
+++ b/docs/actions/projector.update.md
@@ -37,7 +37,7 @@
used_as_default_projector_for_countdown_in_meeting_id: Id,
used_as_default_projector_for_assignment_poll_in_meeting_id: Id,
used_as_default_projector_for_motion_poll_in_meeting_id: Id,
- used_as_default_projector_for_poll_in_meeting_id: Id,
+ used_as_default_projector_for_topic_poll_in_meeting_id: Id,
}
```
diff --git a/meta b/meta
index d16e66eb6a..5f42bf2fec 160000
--- a/meta
+++ b/meta
@@ -1 +1 @@
-Subproject commit d16e66eb6a061264f5c799df79556e9efe7643d6
+Subproject commit 5f42bf2fecc78a4d2520d199ee45d273357fa525
diff --git a/openslides_backend/action/actions/__init__.py b/openslides_backend/action/actions/__init__.py
index 61b30f9904..20ee012362 100644
--- a/openslides_backend/action/actions/__init__.py
+++ b/openslides_backend/action/actions/__init__.py
@@ -20,6 +20,7 @@ def prepare_actions_map() -> None:
mediafile,
meeting,
meeting_mediafile,
+ meeting_poll_default,
meeting_user,
motion,
motion_block,
diff --git a/openslides_backend/action/actions/meeting/create.py b/openslides_backend/action/actions/meeting/create.py
index 03f3b8f706..e8107fd6c3 100644
--- a/openslides_backend/action/actions/meeting/create.py
+++ b/openslides_backend/action/actions/meeting/create.py
@@ -1,7 +1,7 @@
from datetime import datetime, timezone
from typing import Any
-from openslides_backend.models.models import Meeting
+from openslides_backend.models.models import Meeting, MeetingPollDefault
from ....i18n.translator import Translator
from ....i18n.translator import translate as _
@@ -17,6 +17,7 @@
from ...util.default_schema import DefaultSchema
from ...util.register import register_action
from ..group.create import GroupCreate
+from ..meeting_poll_default.create import MeetingPollDefaultCreate
from ..meeting_user.create import MeetingUserCreate
from ..motion_workflow.create import (
MotionWorkflowCreateComplexWorkflowAction,
@@ -171,11 +172,17 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
instance["default_group_id"] = id_from_fqid(fqid_default_group)
instance["admin_group_id"] = id_from_fqid(fqid_admin_group)
- instance["assignment_poll_default_group_ids"] = [
- id_from_fqid(fqid_delegates_group)
- ]
- instance["motion_poll_default_group_ids"] = [id_from_fqid(fqid_delegates_group)]
- instance["topic_poll_default_group_ids"] = [id_from_fqid(fqid_delegates_group)]
+ self.execute_other_action(
+ MeetingPollDefaultCreate,
+ [
+ {
+ "meeting_id": instance["id"],
+ poll_type_field: instance["id"],
+ "group_ids": [id_from_fqid(fqid_delegates_group)],
+ }
+ for poll_type_field in MeetingPollDefault.POLL_TYPE_FIELDS
+ ],
+ )
# Add user to admin group
if admin_ids := instance.pop("admin_ids", []):
diff --git a/openslides_backend/action/actions/meeting/update.py b/openslides_backend/action/actions/meeting/update.py
index 0cece9eb79..399ef7a949 100644
--- a/openslides_backend/action/actions/meeting/update.py
+++ b/openslides_backend/action/actions/meeting/update.py
@@ -1,3 +1,4 @@
+from collections import defaultdict
from datetime import datetime, timezone
from typing import Any, cast
@@ -5,6 +6,7 @@
from ....i18n.translator import Translator
from ....i18n.translator import translate as _
+from ....models.mixins import POLL_TYPES
from ....models.models import Meeting
from ....permissions.management_levels import OrganizationManagementLevel
from ....permissions.permission_helper import (
@@ -24,6 +26,9 @@
from ...util.default_schema import DefaultSchema
from ...util.register import register_action
from ..group.create import GroupCreate
+from ..meeting_poll_default.create import MeetingPollDefaultCreate
+from ..meeting_poll_default.helper_mixin import meeting_poll_default_schema
+from ..meeting_poll_default.update import MeetingPollDefaultUpdate
from .mixins import GetMeetingIdFromIdMixin, MeetingCheckTimesMixin
meeting_settings_keys = [
@@ -128,14 +133,8 @@
"motions_export_follow_recommendation",
"motions_enable_restricted_editor_for_manager",
"motions_enable_restricted_editor_for_non_manager",
- "motion_poll_ballot_paper_selection",
- "motion_poll_ballot_paper_number",
- "motion_poll_default_allow_abstain",
- "motion_poll_default_type",
- "motion_poll_default_onehundred_percent_base",
- "motion_poll_default_group_ids",
- "motion_poll_projection_name_order_first",
- "motion_poll_projection_max_columns",
+ "poll_projection_name_order_first",
+ "poll_projection_max_columns",
"users_enable_presence_view",
"users_enable_vote_weight",
"users_enable_vote_delegations",
@@ -155,20 +154,21 @@
"users_forbid_delegator_to_vote",
"assignments_export_title",
"assignments_export_preamble",
- "assignment_poll_ballot_paper_selection",
- "assignment_poll_ballot_paper_number",
- "assignment_poll_add_candidates_to_list_of_speakers",
- "assignment_poll_enable_max_votes_per_option",
- "assignment_poll_sort_poll_result_by_votes",
- "assignment_poll_default_type",
- "assignment_poll_default_method",
- "assignment_poll_default_onehundred_percent_base",
- "assignment_poll_default_group_ids",
- "topic_poll_default_group_ids",
+ "poll_enable_max_votes_per_option",
"poll_default_live_voting_enabled",
"poll_default_allow_invalid",
+ "poll_default_allow_vote_split",
+ "assignment_poll_add_candidates_to_list_of_speakers",
+ "assignment_poll_default_method",
+ "topic_poll_default_method",
]
+meeting_poll_default_fields = {
+ f"{poll_type}_poll_default_{field_name}": type_
+ for poll_type in POLL_TYPES
+ for field_name, type_ in meeting_poll_default_schema.items()
+}
+
@register_action("meeting.update")
class MeetingUpdate(
@@ -197,6 +197,7 @@ class MeetingUpdate(
],
additional_optional_fields={
"set_as_template": {"type": "boolean"},
+ **meeting_poll_default_fields,
},
)
check_email_field = "users_email_replyto"
@@ -332,6 +333,46 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
if (translations := instance.get("custom_translations")) is not None:
instance["custom_translations"] = Jsonb(translations)
+ # Set poll defaults
+ poll_default_data: dict[str, dict[str, Any]] = defaultdict(dict)
+ for field in meeting_poll_default_fields.keys():
+ if (value := instance.pop(field, None)) is not None:
+ poll_type, field_name = field.split("_poll_default_")
+ poll_default_data[poll_type][field_name] = value
+
+ if poll_default_data:
+ poll_types_to_poll_defaults_fields: dict[str, str] = {
+ poll_type: f"{poll_type}_poll_config_id"
+ for poll_type in poll_default_data.keys()
+ }
+ db_poll_defaults = self.datastore.get(
+ fqid_from_collection_and_id(self.model.collection, instance["id"]),
+ list(poll_types_to_poll_defaults_fields.values()),
+ lock_result=False,
+ )
+ action_data_create = []
+ action_data_update = []
+
+ for poll_type, data in poll_default_data.items():
+ if existing_id := db_poll_defaults.get(
+ poll_types_to_poll_defaults_fields[poll_type]
+ ):
+ action_data_update.append({"id": existing_id, **data})
+ else:
+ action_data_create.append(
+ {
+ "meeting_id": instance["id"],
+ f"used_as_{poll_type}_poll_config_in_meeting_id": instance[
+ "id"
+ ],
+ **data,
+ }
+ )
+ if action_data_create:
+ self.execute_other_action(MeetingPollDefaultCreate, action_data_create)
+ if action_data_update:
+ self.execute_other_action(MeetingPollDefaultUpdate, action_data_update)
+
instance = super().update_instance(instance)
return instance
diff --git a/openslides_backend/action/actions/meeting_poll_default/__init__.py b/openslides_backend/action/actions/meeting_poll_default/__init__.py
new file mode 100644
index 0000000000..26e86a804e
--- /dev/null
+++ b/openslides_backend/action/actions/meeting_poll_default/__init__.py
@@ -0,0 +1 @@
+from . import create, delete, update # noqa
diff --git a/openslides_backend/action/actions/meeting_poll_default/create.py b/openslides_backend/action/actions/meeting_poll_default/create.py
new file mode 100644
index 0000000000..6348d28ca5
--- /dev/null
+++ b/openslides_backend/action/actions/meeting_poll_default/create.py
@@ -0,0 +1,118 @@
+from typing import Any
+
+from openslides_backend.shared.exceptions import ActionException
+
+from ....models.models import MeetingPollDefault, Poll
+from ....shared.filters import FilterOperator
+from ...generics.create import CreateAction
+from ...util.action_type import ActionType
+from ...util.default_schema import DefaultSchema
+from ...util.register import register_action
+
+
+@register_action("meeting_poll_default.create", action_type=ActionType.BACKEND_INTERNAL)
+class MeetingPollDefaultCreate(CreateAction):
+ """
+ Action to create a meeting_poll_default.
+ """
+
+ model = MeetingPollDefault()
+ schema = DefaultSchema(MeetingPollDefault()).get_create_schema(
+ required_properties=["meeting_id"],
+ optional_properties=[
+ "used_as_assignment_poll_config_in_meeting_id",
+ "used_as_motion_poll_config_in_meeting_id",
+ "used_as_topic_poll_config_in_meeting_id",
+ "allow_abstain",
+ "allow_nota",
+ "display_chart",
+ "group_ids",
+ "onehundred_percent_base",
+ "sort_result_by_votes",
+ "strike_out",
+ "visibility",
+ ],
+ )
+
+ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
+ poll_types_fields = {
+ field_name: value
+ for field_name in MeetingPollDefault.POLL_TYPE_FIELDS
+ if (value := instance.get(field_name))
+ }
+ self.check_exactly_one_of(instance, poll_types_fields)
+
+ # Checks below are reachable only if exactly one poll type field is set
+ poll_type_field_name, poll_type_field_value = next(
+ iter(poll_types_fields.items())
+ )
+ self.check_equal_fields(instance, poll_type_field_name, poll_type_field_value)
+ self.check_prevent_updates(instance, poll_type_field_name)
+
+ if "visibility" not in instance:
+ instance["visibility"] = (
+ Poll.VISIBILITY_MANUALLY
+ if poll_type_field_name == "used_as_topic_poll_config_in_meeting_id"
+ else Poll.VISIBILITY_SECRET
+ )
+
+ if (
+ poll_type_field_name == "used_as_topic_poll_config_in_meeting_id"
+ and "display_chart" not in instance
+ ):
+ instance["display_chart"] = "pie"
+
+ return super().update_instance(instance)
+
+ def check_exactly_one_of(
+ self, instance: dict[str, Any], poll_types_fields: dict[str, int]
+ ) -> None:
+ """
+ Checks that exactly one of the poll type fields is defined for the instance:
+ - used_as_assignment_poll_config_in_meeting_id
+ - used_as_motion_poll_config_in_meeting_id
+ - used_as_topic_poll_config_in_meeting_id
+ """
+ # TODO: replace with exactly_one_of constraint (https://github.com/OpenSlides/openslides-meta/issues/540)
+ if not len(poll_types_fields):
+ raise ActionException(
+ f"One of the fields {MeetingPollDefault.POLL_TYPE_FIELDS} must be set."
+ )
+ if len(poll_types_fields) > 1:
+ raise ActionException(
+ f"Only one of {MeetingPollDefault.POLL_TYPE_FIELDS} can be set."
+ )
+
+ def check_equal_fields(
+ self,
+ instance: dict[str, Any],
+ poll_type_field_name: str,
+ poll_type_field_value: int,
+ ) -> None:
+ """
+ Checks that value in poll type field matches the meeting_id.
+ """
+ # TODO: replace with the updated equal_fields constraint (https://github.com/OpenSlides/openslides-meta/issues/541)
+ if poll_type_field_value != instance["meeting_id"]:
+ raise ActionException(
+ f"Values in fields '{poll_type_field_name}' and 'meeting_id' don't match."
+ )
+
+ def check_prevent_updates(
+ self, instance: dict[str, Any], poll_type_field_name: str
+ ) -> None:
+ """
+ Prevents creating a new meeting_poll_default item if meeting already
+ contains meeting_poll_default for the same poll type.
+ """
+ # TODO: replace with the updated prevent_updates constraint (https://github.com/OpenSlides/openslides-meta/issues/542)
+ meeting_id = instance["meeting_id"]
+ if self.datastore.filter(
+ "meeting_poll_default",
+ FilterOperator(poll_type_field_name, "=", meeting_id),
+ ["id"],
+ lock_result=False,
+ ):
+ raise ActionException(
+ f"'{poll_type_field_name}' already exists in meeting/{meeting_id}."
+ )
diff --git a/openslides_backend/action/actions/meeting_poll_default/delete.py b/openslides_backend/action/actions/meeting_poll_default/delete.py
new file mode 100644
index 0000000000..ce2648bd1e
--- /dev/null
+++ b/openslides_backend/action/actions/meeting_poll_default/delete.py
@@ -0,0 +1,15 @@
+from ....models.models import MeetingPollDefault
+from ...generics.delete import DeleteAction
+from ...util.action_type import ActionType
+from ...util.default_schema import DefaultSchema
+from ...util.register import register_action
+
+
+@register_action("meeting_poll_default.delete", action_type=ActionType.BACKEND_INTERNAL)
+class MeetingPollDefaultDelete(DeleteAction):
+ """
+ Action to delete a meeting_poll_default.
+ """
+
+ model = MeetingPollDefault()
+ schema = DefaultSchema(MeetingPollDefault()).get_delete_schema()
diff --git a/openslides_backend/action/actions/meeting_poll_default/helper_mixin.py b/openslides_backend/action/actions/meeting_poll_default/helper_mixin.py
new file mode 100644
index 0000000000..1cadf2e2be
--- /dev/null
+++ b/openslides_backend/action/actions/meeting_poll_default/helper_mixin.py
@@ -0,0 +1,16 @@
+from ....shared.typing import Schema
+
+meeting_poll_default_schema: Schema = {
+ "allow_abstain": {"type": "boolean"},
+ "allow_nota": {"type": "boolean"},
+ "display_chart": {"type": "string"},
+ "group_ids": {
+ "type": "array",
+ "items": {"type": "integer"},
+ "uniqueItems": True,
+ },
+ "onehundred_percent_base": {"type": "string"},
+ "sort_result_by_votes": {"type": "boolean"},
+ "strike_out": {"type": "boolean"},
+ "visibility": {"type": "string"},
+}
diff --git a/openslides_backend/action/actions/meeting_poll_default/update.py b/openslides_backend/action/actions/meeting_poll_default/update.py
new file mode 100644
index 0000000000..6801b9e92a
--- /dev/null
+++ b/openslides_backend/action/actions/meeting_poll_default/update.py
@@ -0,0 +1,26 @@
+from ....models.models import MeetingPollDefault
+from ...generics.update import UpdateAction
+from ...util.action_type import ActionType
+from ...util.default_schema import DefaultSchema
+from ...util.register import register_action
+
+
+@register_action("meeting_poll_default.update", action_type=ActionType.BACKEND_INTERNAL)
+class MeetingPollDefaultUpdate(UpdateAction):
+ """
+ Action to update a meeting_poll_default.
+ """
+
+ model = MeetingPollDefault()
+ schema = DefaultSchema(MeetingPollDefault()).get_update_schema(
+ optional_properties=[
+ "allow_abstain",
+ "allow_nota",
+ "display_chart",
+ "group_ids",
+ "onehundred_percent_base",
+ "sort_result_by_votes",
+ "strike_out",
+ "visibility",
+ ]
+ )
diff --git a/openslides_backend/models/mixins.py b/openslides_backend/models/mixins.py
index 81ffca1633..52dd1cb51b 100644
--- a/openslides_backend/models/mixins.py
+++ b/openslides_backend/models/mixins.py
@@ -18,9 +18,11 @@ class AgendaItemModelMixin:
"countdown",
"assignment_poll",
"motion_poll",
- "poll",
+ "topic_poll",
)
+POLL_TYPES = ("assignment", "motion", "topic")
+
class MeetingModelMixin:
LOGO_PLACES = (
@@ -74,6 +76,12 @@ def reverse_default_projectors(cls) -> list[str]:
]
+class MeetingPollDefaultModelMixin:
+ POLL_TYPE_FIELDS = [
+ f"used_as_{poll_type}_poll_config_in_meeting_id" for poll_type in POLL_TYPES
+ ]
+
+
class PollModelMixin:
STATE_CREATED = "created"
STATE_STARTED = "started"
diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py
index 5cf7155e92..266575c149 100644
--- a/openslides_backend/models/models.py
+++ b/openslides_backend/models/models.py
@@ -2,7 +2,12 @@
from . import fields
from .base import Model
-from .mixins import AgendaItemModelMixin, MeetingModelMixin, PollModelMixin
+from .mixins import (
+ AgendaItemModelMixin,
+ MeetingModelMixin,
+ MeetingPollDefaultModelMixin,
+ PollModelMixin,
+)
class ActionWorker(Model):
@@ -511,17 +516,16 @@ class Group(Model):
is_primary=True,
write_fields=("nm_group_poll_ids_poll_t", "group_id", "poll_id", []),
)
- used_as_motion_poll_default_id = fields.RelationField(
- to={"meeting": "motion_poll_default_group_ids"}
- )
- used_as_assignment_poll_default_id = fields.RelationField(
- to={"meeting": "assignment_poll_default_group_ids"}
- )
- used_as_topic_poll_default_id = fields.RelationField(
- to={"meeting": "topic_poll_default_group_ids"}
- )
- used_as_poll_default_id = fields.RelationField(
- to={"meeting": "poll_default_group_ids"}
+ used_in_meeting_poll_default_ids = fields.RelationListField(
+ to={"meeting_poll_default": "group_ids"},
+ is_view_field=True,
+ is_primary=True,
+ write_fields=(
+ "nm_group_uimpdi_meeting_poll_default_t",
+ "group_id",
+ "meeting_poll_default_id",
+ [],
+ ),
)
meeting_id = fields.RelationField(
to={"meeting": "group_ids"}, required=True, constant=True
@@ -877,41 +881,6 @@ class Meeting(Model, MeetingModelMixin):
motions_export_follow_recommendation = fields.BooleanField(default=False)
motions_enable_restricted_editor_for_manager = fields.BooleanField()
motions_enable_restricted_editor_for_non_manager = fields.BooleanField(default=True)
- motion_poll_ballot_paper_selection = fields.CharField(
- default="CUSTOM_NUMBER",
- constraints={
- "enum": [
- "NUMBER_OF_DELEGATES",
- "NUMBER_OF_ALL_PARTICIPANTS",
- "CUSTOM_NUMBER",
- ]
- },
- )
- motion_poll_ballot_paper_number = fields.IntegerField(default=8)
- motion_poll_default_type = fields.CharField(default="secret")
- motion_poll_default_allow_abstain = fields.BooleanField(default=True)
- motion_poll_default_onehundred_percent_base = fields.CharField(
- default="valid",
- constraints={
- "enum": [
- "yes_no",
- "valid",
- "cast",
- "entitled",
- "entitled_present",
- "disabled",
- ]
- },
- )
- motion_poll_default_group_ids = fields.RelationListField(
- to={"group": "used_as_motion_poll_default_id"}, is_view_field=True
- )
- motion_poll_projection_name_order_first = fields.CharField(
- required=True,
- default="last_name",
- constraints={"enum": ["first_name", "last_name"]},
- )
- motion_poll_projection_max_columns = fields.IntegerField(required=True, default=6)
meeting_user_ids = fields.RelationListField(
to={"meeting_user": "meeting_id"},
on_delete=fields.OnDelete.CASCADE,
@@ -942,70 +911,43 @@ class Meeting(Model, MeetingModelMixin):
users_forbid_delegator_to_vote = fields.BooleanField()
assignments_export_title = fields.CharField(default="Elections")
assignments_export_preamble = fields.TextField()
- assignment_poll_ballot_paper_selection = fields.CharField(
- default="CUSTOM_NUMBER",
- constraints={
- "enum": [
- "NUMBER_OF_DELEGATES",
- "NUMBER_OF_ALL_PARTICIPANTS",
- "CUSTOM_NUMBER",
- ]
- },
- )
- assignment_poll_ballot_paper_number = fields.IntegerField(default=8)
assignment_poll_add_candidates_to_list_of_speakers = fields.BooleanField(
default=False
)
- assignment_poll_enable_max_votes_per_option = fields.BooleanField(default=False)
- assignment_poll_sort_poll_result_by_votes = fields.BooleanField(default=True)
- assignment_poll_default_type = fields.CharField(default="secret")
- assignment_poll_default_method = fields.CharField(default="selection")
- assignment_poll_default_onehundred_percent_base = fields.CharField(
- default="valid",
+ assignment_poll_default_method = fields.CharField(
+ default="selection",
constraints={
"enum": [
- "no_general",
- "yes_no",
- "valid",
- "cast",
- "entitled",
- "entitled_present",
- "disabled",
+ "approval",
+ "selection",
+ "rating_score",
+ "rating_approval",
+ "stv_scottish",
]
},
)
- assignment_poll_default_group_ids = fields.RelationListField(
- to={"group": "used_as_assignment_poll_default_id"}, is_view_field=True
+ assignment_poll_config_id = fields.RelationField(
+ to={"meeting_poll_default": "used_as_assignment_poll_config_in_meeting_id"}
)
- poll_ballot_paper_selection = fields.CharField(
- constraints={
- "enum": [
- "NUMBER_OF_DELEGATES",
- "NUMBER_OF_ALL_PARTICIPANTS",
- "CUSTOM_NUMBER",
- ]
- }
+ motion_poll_config_id = fields.RelationField(
+ to={"meeting_poll_default": "used_as_motion_poll_config_in_meeting_id"}
)
- poll_ballot_paper_number = fields.IntegerField()
- poll_sort_poll_result_by_votes = fields.BooleanField()
- poll_default_type = fields.CharField(default="analog")
- poll_default_method = fields.CharField()
- poll_default_onehundred_percent_base = fields.CharField(
- default="valid",
+ topic_poll_default_method = fields.CharField(
+ default="selection",
constraints={
"enum": [
- "no_general",
- "valid",
- "cast",
- "entitled",
- "entitled_present",
- "disabled",
+ "approval",
+ "selection",
+ "rating_score",
+ "rating_approval",
+ "stv_scottish",
]
},
)
- poll_default_group_ids = fields.RelationListField(
- to={"group": "used_as_poll_default_id"}, is_view_field=True
+ topic_poll_config_id = fields.RelationField(
+ to={"meeting_poll_default": "used_as_topic_poll_config_in_meeting_id"}
)
+ poll_enable_max_votes_per_option = fields.BooleanField(default=False)
poll_default_live_voting_enabled = fields.BooleanField(
default=False,
constraints={
@@ -1015,19 +957,22 @@ class Meeting(Model, MeetingModelMixin):
poll_default_allow_invalid = fields.BooleanField(
default=False,
constraints={
- "description": "Defines defaut `poll.allow_invalid` option suggested to user."
+ "description": "Defines default `poll.allow_invalid` option suggested to user."
},
)
poll_default_allow_vote_split = fields.BooleanField(
default=False,
constraints={
- "description": "Defines defaut `poll.allow_vote_split` option suggested to user."
+ "description": "Defines default `poll.allow_vote_split` option suggested to user."
},
)
poll_couple_countdown = fields.BooleanField(default=True)
- topic_poll_default_group_ids = fields.RelationListField(
- to={"group": "used_as_topic_poll_default_id"}, is_view_field=True
+ poll_projection_name_order_first = fields.CharField(
+ required=True,
+ default="last_name",
+ constraints={"enum": ["first_name", "last_name"]},
)
+ poll_projection_max_columns = fields.IntegerField(required=True, default=6)
projector_ids = fields.RelationListField(
to={"projector": "meeting_id"},
on_delete=fields.OnDelete.CASCADE,
@@ -1167,6 +1112,11 @@ class Meeting(Model, MeetingModelMixin):
is_view_field=True,
is_primary=True,
)
+ poll_default_ids = fields.RelationListField(
+ to={"meeting_poll_default": "meeting_id"},
+ on_delete=fields.OnDelete.CASCADE,
+ is_view_field=True,
+ )
assignment_ids = fields.RelationListField(
to={"assignment": "meeting_id"},
on_delete=fields.OnDelete.CASCADE,
@@ -1363,8 +1313,8 @@ class Meeting(Model, MeetingModelMixin):
is_view_field=True,
required=True,
)
- default_projector_poll_ids = fields.RelationListField(
- to={"projector": "used_as_default_projector_for_poll_in_meeting_id"},
+ default_projector_topic_poll_ids = fields.RelationListField(
+ to={"projector": "used_as_default_projector_for_topic_poll_in_meeting_id"},
is_view_field=True,
required=True,
)
@@ -1500,6 +1450,45 @@ class MeetingMediafile(Model):
)
+class MeetingPollDefault(Model, MeetingPollDefaultModelMixin):
+ collection = "meeting_poll_default"
+ verbose_name = "meeting poll default"
+ managed_by = "backend"
+
+ id = fields.IntegerField(required=True, constant=True)
+ sort_result_by_votes = fields.BooleanField(default=True)
+ visibility = fields.CharField(
+ constraints={"enum": ["manually", "named", "open", "secret"]}
+ )
+ allow_abstain = fields.BooleanField(default=True)
+ allow_nota = fields.BooleanField(default=False)
+ strike_out = fields.BooleanField(default=False)
+ onehundred_percent_base = fields.CharField(default="valid")
+ group_ids = fields.RelationListField(
+ to={"group": "used_in_meeting_poll_default_ids"},
+ is_view_field=True,
+ write_fields=(
+ "nm_group_uimpdi_meeting_poll_default_t",
+ "meeting_poll_default_id",
+ "group_id",
+ [],
+ ),
+ )
+ display_chart = fields.CharField()
+ used_as_assignment_poll_config_in_meeting_id = fields.RelationField(
+ to={"meeting": "assignment_poll_config_id"}, is_view_field=True, constant=True
+ )
+ used_as_motion_poll_config_in_meeting_id = fields.RelationField(
+ to={"meeting": "motion_poll_config_id"}, is_view_field=True, constant=True
+ )
+ used_as_topic_poll_config_in_meeting_id = fields.RelationField(
+ to={"meeting": "topic_poll_config_id"}, is_view_field=True, constant=True
+ )
+ meeting_id = fields.RelationField(
+ to={"meeting": "poll_default_ids"}, required=True, constant=True
+ )
+
+
class MeetingUser(Model):
collection = "meeting_user"
verbose_name = "meeting user"
@@ -2705,8 +2694,8 @@ class Projector(Model):
used_as_default_projector_for_motion_poll_in_meeting_id = fields.RelationField(
to={"meeting": "default_projector_motion_poll_ids"}
)
- used_as_default_projector_for_poll_in_meeting_id = fields.RelationField(
- to={"meeting": "default_projector_poll_ids"}
+ used_as_default_projector_for_topic_poll_in_meeting_id = fields.RelationField(
+ to={"meeting": "default_projector_topic_poll_ids"}
)
meeting_id = fields.RelationField(
to={"meeting": "projector_ids"}, required=True, constant=True
diff --git a/tests/database/reader/system/test_filter.py b/tests/database/reader/system/test_filter.py
index 42c6bdea42..4a2a7f2ab3 100644
--- a/tests/database/reader/system/test_filter.py
+++ b/tests/database/reader/system/test_filter.py
@@ -237,10 +237,7 @@ def test_types_enum_list(db_connection: Connection) -> None:
"poll_ids": None,
"read_chat_group_ids": None,
"read_comment_section_ids": None,
- "used_as_assignment_poll_default_id": None,
- "used_as_motion_poll_default_id": None,
- "used_as_poll_default_id": None,
- "used_as_topic_poll_default_id": None,
+ "used_in_meeting_poll_default_ids": None,
"weight": None,
"write_chat_group_ids": None,
"write_comment_section_ids": None,
diff --git a/tests/database/reader/system/util.py b/tests/database/reader/system/util.py
index 6088b202f0..faf0a48b72 100644
--- a/tests/database/reader/system/util.py
+++ b/tests/database/reader/system/util.py
@@ -78,12 +78,8 @@
"meeting_ids": None,
"is_present_in_meeting_ids": None,
"meeting_user_ids": None,
- # "poll_candidate_ids": None,
- # "poll_voted_ids": None,
- # "acting_vote_ids": None,
"committee_ids": None,
"committee_management_ids": None,
- # "represented_vote_ids": None,
"organization_id": 1,
},
2: {
@@ -114,12 +110,8 @@
"meeting_ids": None,
"is_present_in_meeting_ids": None,
"meeting_user_ids": None,
- # "poll_candidate_ids": None,
- # "poll_voted_ids": None,
- # "acting_vote_ids": None,
"committee_ids": None,
"committee_management_ids": None,
- # "represented_vote_ids": None,
"organization_id": 1,
},
3: {
@@ -150,12 +142,8 @@
"meeting_ids": None,
"is_present_in_meeting_ids": None,
"meeting_user_ids": None,
- # "poll_candidate_ids": None,
- # "poll_voted_ids": None,
- # "acting_vote_ids": None,
"committee_ids": None,
"committee_management_ids": None,
- # "represented_vote_ids": None,
"organization_id": 1,
},
},
diff --git a/tests/database/writer/system/test_delete.py b/tests/database/writer/system/test_delete.py
index 18d1e625b3..8eba82523d 100644
--- a/tests/database/writer/system/test_delete.py
+++ b/tests/database/writer/system/test_delete.py
@@ -314,7 +314,7 @@ def test_delete_1_n_not_null_success_replace_relations(
"default_projector_countdown_ids": [2],
"default_projector_assignment_poll_ids": [2],
"default_projector_motion_poll_ids": [2],
- "default_projector_poll_ids": [2],
+ "default_projector_topic_poll_ids": [2],
"reference_projector_id": 2,
},
)
@@ -335,7 +335,7 @@ def test_delete_1_n_not_null_success_replace_relations(
"used_as_default_projector_for_countdown_in_meeting_id": 1,
"used_as_default_projector_for_assignment_poll_in_meeting_id": 1,
"used_as_default_projector_for_motion_poll_in_meeting_id": 1,
- "used_as_default_projector_for_poll_in_meeting_id": 1,
+ "used_as_default_projector_for_topic_poll_in_meeting_id": 1,
"used_as_reference_projector_meeting_id": 1,
},
)
diff --git a/tests/system/action/meeting/test_clone.py b/tests/system/action/meeting/test_clone.py
index be377db4bf..bb35d1c54a 100644
--- a/tests/system/action/meeting/test_clone.py
+++ b/tests/system/action/meeting/test_clone.py
@@ -499,11 +499,6 @@ def test_clone_with_change_recommendation_complex(self) -> None:
"motions_export_preamble": "an export preamble",
"motions_export_submitter_recommendation": True,
"motions_export_follow_recommendation": True,
- "motion_poll_ballot_paper_selection": "CUSTOM_NUMBER",
- "motion_poll_ballot_paper_number": 8,
- "motion_poll_default_type": "pseudoanonymous",
- "motion_poll_default_onehundred_percent_base": "valid",
- "motion_poll_default_group_ids": [],
},
)
self.set_user_groups(1, [5])
@@ -1405,10 +1400,6 @@ def test_clone_with_settings(self) -> None:
"motions_export_preamble": "pre",
"motions_export_submitter_recommendation": True,
"motions_export_follow_recommendation": True,
- "motion_poll_ballot_paper_selection": "NUMBER_OF_DELEGATES",
- "motion_poll_ballot_paper_number": 42,
- "motion_poll_default_type": "pseudoanonymous",
- "motion_poll_default_onehundred_percent_base": "yes_no",
"users_enable_presence_view": True,
"users_enable_vote_weight": True,
"users_enable_vote_delegations": True,
@@ -1424,15 +1415,41 @@ def test_clone_with_settings(self) -> None:
"users_email_body": "body",
"assignments_export_title": "title",
"assignments_export_preamble": "pre",
- "assignment_poll_ballot_paper_selection": "NUMBER_OF_DELEGATES",
- "assignment_poll_ballot_paper_number": 42,
+ "assignment_poll_default_method": "rating_approval",
"assignment_poll_add_candidates_to_list_of_speakers": True,
- "assignment_poll_enable_max_votes_per_option": False,
- "assignment_poll_sort_poll_result_by_votes": True,
- "assignment_poll_default_type": "pseudoanonymous",
- "assignment_poll_default_method": "YNA",
- "assignment_poll_default_onehundred_percent_base": "valid",
+ "poll_enable_max_votes_per_option": False,
}
+ assignment_poll_settings = {
+ "sort_result_by_votes": True,
+ "visibility": "secret",
+ "onehundred_percent_base": "valid",
+ "allow_abstain": True,
+ }
+ motion_poll_settings = {
+ "visibility": "secret",
+ "onehundred_percent_base": "valid",
+ "allow_abstain": False,
+ }
+ topic_poll_settings = {
+ "sort_result_by_votes": True,
+ "visibility": "named",
+ "onehundred_percent_base": "valid",
+ "display_chart": "pie",
+ }
+ self.test_models.update(
+ {
+ "meeting/1": {
+ "assignment_poll_config_id": 1,
+ "motion_poll_config_id": 2,
+ "topic_poll_config_id": 3,
+ },
+ "group/2": {"used_in_meeting_poll_default_ids": [1, 2]},
+ "group/3": {"used_in_meeting_poll_default_ids": [1, 2, 3]},
+ "meeting_poll_default/1": {"meeting_id": 1, **assignment_poll_settings},
+ "meeting_poll_default/2": {"meeting_id": 1, **motion_poll_settings},
+ "meeting_poll_default/3": {"meeting_id": 1, **topic_poll_settings},
+ }
+ )
self.meeting_data.update(settings)
self.set_test_data_with_admin()
response = self.request("meeting.clone", {"meeting_id": 1})
@@ -1440,7 +1457,49 @@ def test_clone_with_settings(self) -> None:
settings["name"] = f"{settings['name']} - Copy"
settings["start_time"] = timestamp_with_tz
settings["end_time"] = timestamp_with_tz
- self.assert_model_exists("meeting/2", settings)
+ self.assert_model_exists(
+ "meeting/2",
+ {
+ **settings,
+ "assignment_poll_config_id": 4,
+ "motion_poll_config_id": 5,
+ "topic_poll_config_id": 6,
+ "poll_default_ids": [4, 5, 6],
+ },
+ )
+ self.assert_model_exists(
+ "meeting_poll_default/4",
+ {
+ "meeting_id": 2,
+ "used_as_assignment_poll_config_in_meeting_id": 2,
+ "group_ids": [5, 6],
+ **assignment_poll_settings,
+ },
+ )
+ self.assert_model_exists(
+ "meeting_poll_default/5",
+ {
+ "meeting_id": 2,
+ "used_as_motion_poll_config_in_meeting_id": 2,
+ "group_ids": [5, 6],
+ **motion_poll_settings,
+ },
+ )
+ self.assert_model_exists(
+ "meeting_poll_default/6",
+ {
+ "meeting_id": 2,
+ "used_as_topic_poll_config_in_meeting_id": 2,
+ "group_ids": [6],
+ **topic_poll_settings,
+ },
+ )
+ self.assert_model_exists(
+ "group/5", {"meeting_id": 2, "used_in_meeting_poll_default_ids": [4, 5]}
+ )
+ self.assert_model_exists(
+ "group/6", {"meeting_id": 2, "used_in_meeting_poll_default_ids": [4, 5, 6]}
+ )
def test_limit_of_meetings_error(self) -> None:
self.set_test_data_with_admin()
diff --git a/tests/system/action/meeting/test_create.py b/tests/system/action/meeting/test_create.py
index 8399fe120d..02b87c4e78 100644
--- a/tests/system/action/meeting/test_create.py
+++ b/tests/system/action/meeting/test_create.py
@@ -70,18 +70,22 @@ def test_create_simple_and_complex_workflow(self) -> None:
"projector_countdown_warning_time": 0,
"organization_tag_ids": [3],
"is_active_in_organization_id": 1,
- "assignment_poll_default_group_ids": [3],
- "motion_poll_default_group_ids": [3],
- "topic_poll_default_group_ids": [3],
- "motion_poll_projection_name_order_first": "last_name",
- "motion_poll_projection_max_columns": 6,
+ "poll_default_ids": [1, 2, 3],
+ "assignment_poll_config_id": 1,
+ "motion_poll_config_id": 2,
+ "topic_poll_config_id": 3,
+ "poll_projection_name_order_first": "last_name",
+ "poll_projection_max_columns": 6,
**{field: [1] for field in Meeting.all_default_projectors()},
},
)
self.assert_model_exists(ONE_ORGANIZATION_FQID, {"active_meeting_ids": [1]})
self.assert_model_exists("group/1", {"name": "Default"})
self.assert_model_exists("group/2", {"name": "Admin"})
- self.assert_model_exists("group/3", {"name": "Delegates"})
+ self.assert_model_exists(
+ "group/3",
+ {"name": "Delegates", "used_in_meeting_poll_default_ids": [1, 2, 3]},
+ )
self.assert_model_exists("group/4", {"name": "Staff"})
self.assert_model_exists(
"motion_workflow/1",
@@ -185,9 +189,60 @@ def test_create_simple_and_complex_workflow(self) -> None:
"countdown_time": 60,
},
)
+ self.assert_model_exists(
+ "meeting_poll_default/1",
+ {
+ "meeting_id": 1,
+ "used_as_assignment_poll_config_in_meeting_id": 1,
+ "used_as_motion_poll_config_in_meeting_id": None,
+ "used_as_topic_poll_config_in_meeting_id": None,
+ "group_ids": [3],
+ "sort_result_by_votes": True,
+ "visibility": Poll.VISIBILITY_SECRET,
+ "allow_abstain": True,
+ "allow_nota": False,
+ "strike_out": False,
+ "onehundred_percent_base": "valid",
+ "display_chart": None,
+ },
+ )
+ self.assert_model_exists(
+ "meeting_poll_default/2",
+ {
+ "meeting_id": 1,
+ "used_as_assignment_poll_config_in_meeting_id": None,
+ "used_as_motion_poll_config_in_meeting_id": 1,
+ "used_as_topic_poll_config_in_meeting_id": None,
+ "group_ids": [3],
+ "sort_result_by_votes": True,
+ "visibility": Poll.VISIBILITY_SECRET,
+ "allow_abstain": True,
+ "allow_nota": False,
+ "strike_out": False,
+ "onehundred_percent_base": "valid",
+ "display_chart": None,
+ },
+ )
+ self.assert_model_exists(
+ "meeting_poll_default/3",
+ {
+ "meeting_id": 1,
+ "used_as_assignment_poll_config_in_meeting_id": None,
+ "used_as_motion_poll_config_in_meeting_id": None,
+ "used_as_topic_poll_config_in_meeting_id": 1,
+ "group_ids": [3],
+ "sort_result_by_votes": True,
+ "visibility": Poll.VISIBILITY_MANUALLY,
+ "allow_abstain": True,
+ "allow_nota": False,
+ "strike_out": False,
+ "onehundred_percent_base": "valid",
+ "display_chart": "pie",
+ },
+ )
def test_check_action_data_fields(self) -> None:
- """Also checks defaults for assignment_poll and motion_poll."""
+ """Also checks defaults for polls."""
external_id = "external"
self.basic_test(
{
@@ -206,9 +261,11 @@ def test_check_action_data_fields(self) -> None:
"start_time": datetime.fromtimestamp(1608120653, ZoneInfo("UTC")),
"end_time": datetime.fromtimestamp(1608121653, ZoneInfo("UTC")),
"external_id": external_id,
- "assignment_poll_default_type": Poll.VISIBILITY_SECRET,
"assignment_poll_default_method": Poll.METHOD_SELECTION,
- "motion_poll_default_type": Poll.VISIBILITY_SECRET,
+ "topic_poll_default_method": Poll.METHOD_SELECTION,
+ "poll_default_live_voting_enabled": False,
+ "poll_default_allow_invalid": False,
+ "poll_default_allow_vote_split": False,
},
)
diff --git a/tests/system/action/meeting/test_delete.py b/tests/system/action/meeting/test_delete.py
index 1f4ea2ae4c..7c19ab287e 100644
--- a/tests/system/action/meeting/test_delete.py
+++ b/tests/system/action/meeting/test_delete.py
@@ -437,6 +437,8 @@ def test_delete_full_meeting(self) -> None:
self.assert_model_not_exists(f"motion_state/{i+1}")
for i in range(2):
self.assert_model_not_exists(f"motion_workflow/{i+1}")
+ for i in range(3):
+ self.assert_model_not_exists(f"meeting_poll_default/{i+1}")
for i in range(5):
self.assert_model_not_exists(f"poll/{i+1}")
for i in range(10):
diff --git a/tests/system/action/meeting/test_import.py b/tests/system/action/meeting/test_import.py
index bd79164c9b..b19d851e8a 100644
--- a/tests/system/action/meeting/test_import.py
+++ b/tests/system/action/meeting/test_import.py
@@ -137,11 +137,6 @@ def create_request_data(
"motions_export_preamble": "an export preamble",
"motions_export_submitter_recommendation": True,
"motions_export_follow_recommendation": True,
- "motion_poll_ballot_paper_selection": "CUSTOM_NUMBER",
- "motion_poll_ballot_paper_number": 8,
- "motion_poll_default_type": "pseudoanonymous",
- "motion_poll_default_onehundred_percent_base": "valid",
- "motion_poll_default_group_ids": [],
"users_enable_presence_view": True,
"users_enable_vote_weight": True,
"users_enable_vote_delegations": True,
@@ -157,22 +152,8 @@ def create_request_data(
"users_email_body": "Dear {name},\n\nthis is your personal OpenSlides login:\n\n{url}\nUsername: {username}\nPassword: {password}\n\n\nThis email was generated automatically.",
"assignments_export_title": "Elections",
"assignments_export_preamble": "",
- "assignment_poll_ballot_paper_selection": "CUSTOM_NUMBER",
- "assignment_poll_ballot_paper_number": 8,
"assignment_poll_add_candidates_to_list_of_speakers": True,
- "assignment_poll_enable_max_votes_per_option": None,
- "assignment_poll_sort_poll_result_by_votes": True,
- "assignment_poll_default_type": "pseudoanonymous",
- "assignment_poll_default_method": "votes",
- "assignment_poll_default_onehundred_percent_base": "valid",
- "assignment_poll_default_group_ids": [],
- "poll_ballot_paper_selection": "CUSTOM_NUMBER",
- "poll_ballot_paper_number": 8,
- "poll_sort_poll_result_by_votes": True,
- "poll_default_type": "pseudoanonymous",
- "poll_default_method": "votes",
- "poll_default_onehundred_percent_base": "valid",
- "poll_default_group_ids": [],
+ "poll_enable_max_votes_per_option": None,
"poll_default_live_voting_enabled": False,
"poll_default_allow_invalid": False,
"poll_couple_countdown": True,
@@ -367,9 +348,7 @@ def get_group_data(self, obj_id: int, data: dict[str, Any] = {}) -> dict[str, An
"read_chat_group_ids": [],
"write_chat_group_ids": [],
"poll_ids": [],
- "used_as_motion_poll_default_id": None,
- "used_as_assignment_poll_default_id": None,
- "used_as_poll_default_id": None,
+ "used_in_meeting_poll_default_ids": [],
**data,
}
@@ -911,6 +890,65 @@ def test_check_usernames_new_and_twice(self) -> None:
)
self.assert_model_not_exists("user/3")
+ def test_with_poll_default_collections(self) -> None:
+ poll_defaults_data: dict[str, dict[str, Any]] = {
+ "meeting": {
+ "1": {
+ "poll_default_ids": [1, 2, 3],
+ "assignment_poll_config_id": 1,
+ "motion_poll_config_id": 2,
+ "topic_poll_config_id": 3,
+ }
+ },
+ "group": {"2": {"used_in_meeting_poll_default_ids": [1, 2, 3]}},
+ "meeting_poll_default": {
+ str(id_): {
+ "id": id_,
+ f"used_as_{poll_type}_poll_config_in_meeting_id": 1,
+ "meeting_id": 1,
+ "sort_result_by_votes": False,
+ "visibility": "named",
+ "allow_abstain": False,
+ "allow_nota": True,
+ "strike_out": True,
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_CAST,
+ "group_ids": [2],
+ "display_chart": "test",
+ }
+ for poll_type, id_ in {
+ "assignment": 1,
+ "motion": 2,
+ "topic": 3,
+ }.items()
+ },
+ }
+ request_data = self.create_request_data(poll_defaults_data)
+ response = self.request("meeting.import", request_data)
+ self.assert_status_code(response, 200)
+ self.assert_model_exists(
+ "meeting/2",
+ {
+ "poll_default_ids": [1, 2, 3],
+ "assignment_poll_config_id": 1,
+ "motion_poll_config_id": 2,
+ "topic_poll_config_id": 3,
+ },
+ )
+ self.assert_model_exists(
+ "group/5",
+ {"used_in_meeting_poll_default_ids": [1, 2, 3]},
+ )
+ for poll_type, id_ in {"assignment": 1, "motion": 2, "topic": 3}.items():
+ self.assert_model_exists(
+ f"meeting_poll_default/{id_}",
+ {
+ **poll_defaults_data["meeting_poll_default"][str(id_)],
+ "meeting_id": 2,
+ f"used_as_{poll_type}_poll_config_in_meeting_id": 2,
+ "group_ids": [5],
+ },
+ )
+
def test_check_negative_default_vote_weight(self) -> None:
request_data = self.create_request_data({})
request_data["meeting"]["user"]["1"] = self.get_user_data(
@@ -2774,12 +2812,8 @@ def test_import_export_with_orga_mediafiles(self) -> None:
"motions_supporters_min_amount": 0,
"motions_export_submitter_recommendation": True,
"motions_export_follow_recommendation": False,
- "motion_poll_ballot_paper_selection": "CUSTOM_NUMBER",
- "motion_poll_ballot_paper_number": 8,
- "motion_poll_default_type": "pseudoanonymous",
- "motion_poll_default_onehundred_percent_base": "valid",
- "motion_poll_projection_name_order_first": "last_name",
- "motion_poll_projection_max_columns": 6,
+ "poll_projection_name_order_first": "last_name",
+ "poll_projection_max_columns": 6,
"users_enable_presence_view": False,
"users_enable_vote_weight": False,
"users_enable_vote_delegations": True,
@@ -2790,16 +2824,10 @@ def test_import_export_with_orga_mediafiles(self) -> None:
"users_email_subject": "OpenSlides access data",
"users_email_body": "blablabla",
"assignments_export_title": "Elections",
- "assignment_poll_ballot_paper_selection": "CUSTOM_NUMBER",
- "assignment_poll_ballot_paper_number": 8,
"assignment_poll_add_candidates_to_list_of_speakers": False,
- "assignment_poll_enable_max_votes_per_option": False,
- "assignment_poll_sort_poll_result_by_votes": True,
- "assignment_poll_default_type": "pseudoanonymous",
- "assignment_poll_default_method": "Y",
- "assignment_poll_default_onehundred_percent_base": "valid",
- "poll_default_type": "analog",
- "poll_default_onehundred_percent_base": "valid",
+ "poll_enable_max_votes_per_option": False,
+ "assignment_poll_default_method": Poll.METHOD_APPROVAL,
+ "topic_poll_default_method": Poll.METHOD_SELECTION,
"poll_default_live_voting_enabled": False,
"poll_couple_countdown": True,
**{field: [1] for field in Meeting.all_default_projectors()},
diff --git a/tests/system/action/meeting/test_settings.py b/tests/system/action/meeting/test_settings.py
index ef8d03f430..f07dde5591 100644
--- a/tests/system/action/meeting/test_settings.py
+++ b/tests/system/action/meeting/test_settings.py
@@ -1,3 +1,4 @@
+from openslides_backend.models.models import Poll
from tests.system.action.base import BaseActionTestCase
@@ -6,16 +7,130 @@ def setUp(self) -> None:
super().setUp()
self.create_meeting()
- def test_group_ids(self) -> None:
- self.set_models({"group/1": {"used_as_motion_poll_default_id": 1}})
+ def test_update_poll_default_group_ids(self) -> None:
+ self.set_models(
+ {
+ "meeting/1": {"motion_poll_config_id": 1},
+ "group/2": {"used_in_meeting_poll_default_ids": [1]},
+ "meeting_poll_default/1": {"meeting_id": 1},
+ }
+ )
response = self.request(
"meeting.update", {"id": 1, "motion_poll_default_group_ids": [2, 3]}
)
self.assert_status_code(response, 200)
- self.assert_model_exists("meeting/1", {"motion_poll_default_group_ids": [2, 3]})
- self.assert_model_exists("group/1", {"used_as_motion_poll_default_id": None})
- self.assert_model_exists("group/2", {"used_as_motion_poll_default_id": 1})
- self.assert_model_exists("group/3", {"used_as_motion_poll_default_id": 1})
+ self.assert_model_exists("group/1", {"used_in_meeting_poll_default_ids": None})
+ self.assert_model_exists("group/2", {"used_in_meeting_poll_default_ids": [1]})
+ self.assert_model_exists("group/3", {"used_in_meeting_poll_default_ids": [1]})
+ self.assert_model_exists("meeting_poll_default/1", {"group_ids": [2, 3]})
+
+ def test_set_poll_default_group_ids_not_in_meeting(self) -> None:
+ self.create_meeting(4)
+ response = self.request(
+ "meeting.update", {"id": 1, "motion_poll_default_group_ids": [4]}
+ )
+ self.assert_status_code(response, 400)
+ self.assertEqual(
+ "The following models do not belong to meeting 1: ['group/4']",
+ response.json["message"],
+ )
+ self.assert_model_exists("meeting/1", {"poll_default_ids": None})
+
+ def test_poll_default_projector(self) -> None:
+ self.set_models({"projector/2": {"meeting_id": 1, "name": "Projector 2"}})
+ response = self.request(
+ "meeting.update", {"id": 1, "default_projector_assignment_poll_ids": [2]}
+ )
+ self.assert_status_code(response, 200)
+ self.assert_model_exists(
+ "meeting/1", {"default_projector_assignment_poll_ids": [2]}
+ )
+ self.assert_model_exists(
+ "projector/1",
+ {"used_as_default_projector_for_assignment_poll_in_meeting_id": None},
+ )
+ self.assert_model_exists(
+ "projector/2",
+ {"used_as_default_projector_for_assignment_poll_in_meeting_id": 1},
+ )
+
+ def test_poll_default_projector_not_in_meeting(self) -> None:
+ self.create_meeting(4)
+ response = self.request(
+ "meeting.update", {"id": 1, "default_projector_assignment_poll_ids": [4]}
+ )
+ self.assert_status_code(response, 400)
+ self.assertEqual(
+ "The following models do not belong to meeting 1: ['projector/4']",
+ response.json["message"],
+ )
+ self.assert_model_exists(
+ "meeting/1", {"default_projector_assignment_poll_ids": [1]}
+ )
+
+ def base_test_meeting_poll_default_settings(self) -> None:
+ data = {
+ "sort_result_by_votes": False,
+ "visibility": "named",
+ "allow_abstain": False,
+ "allow_nota": True,
+ "strike_out": True,
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_CAST,
+ "group_ids": [1],
+ "display_chart": "test",
+ }
+ response = self.request(
+ "meeting.update",
+ {
+ "id": 1,
+ **{
+ f"{poll_type}_poll_default_{field_name}": value
+ for poll_type in ("assignment", "motion", "topic")
+ for field_name, value in data.items()
+ },
+ },
+ )
+ self.assert_status_code(response, 200)
+ for poll_type, id_ in {"assignment": 1, "motion": 2, "topic": 3}.items():
+ self.assert_model_exists(
+ f"meeting_poll_default/{id_}",
+ {
+ "meeting_id": 1,
+ f"used_as_{poll_type}_poll_config_in_meeting_id": 1,
+ **data,
+ },
+ )
+ self.assert_model_exists(
+ "group/1",
+ {"used_in_meeting_poll_default_ids": [1, 2, 3]},
+ )
+
+ def test_create_meeting_poll_default_settings(self) -> None:
+ self.base_test_meeting_poll_default_settings()
+ self.assert_model_exists(
+ "meeting/1",
+ {
+ "poll_default_ids": [1, 2, 3],
+ "assignment_poll_config_id": 1,
+ "motion_poll_config_id": 2,
+ "topic_poll_config_id": 3,
+ },
+ )
+
+ def test_update_meeting_poll_default_settings(self) -> None:
+ self.set_models(
+ {
+ "meeting/1": {
+ "assignment_poll_config_id": 1,
+ "motion_poll_config_id": 2,
+ "topic_poll_config_id": 3,
+ },
+ "meeting_poll_default/1": {"meeting_id": 1},
+ "meeting_poll_default/2": {"meeting_id": 1},
+ "meeting_poll_default/3": {"meeting_id": 1},
+ }
+ )
+ self.base_test_meeting_poll_default_settings()
def test_html_field_iframe(self) -> None:
response = self.request(
diff --git a/tests/system/action/meeting/test_update.py b/tests/system/action/meeting/test_update.py
index 17d09aaa7c..59993a9adb 100644
--- a/tests/system/action/meeting/test_update.py
+++ b/tests/system/action/meeting/test_update.py
@@ -74,8 +74,8 @@ def test_update_some_fields_user_email(self) -> None:
def test_update_motion_poll_projection(self) -> None:
data = {
- "motion_poll_projection_name_order_first": "first_name",
- "motion_poll_projection_max_columns": 5,
+ "poll_projection_name_order_first": "first_name",
+ "poll_projection_max_columns": 5,
}
self.basic_test(data)
self.assert_model_exists("meeting/1", data)
@@ -83,12 +83,12 @@ def test_update_motion_poll_projection(self) -> None:
def test_update_motion_poll_projection_invalid_data_error(self) -> None:
response_message = self.basic_test(
{
- "motion_poll_projection_name_order_first": "best_name",
+ "poll_projection_name_order_first": "best_name",
},
check_200=False,
)
self.assertEqual(
- "Action meeting.update: data.motion_poll_projection_name_order_first must be one of ['first_name', 'last_name']",
+ "Action meeting.update: data.poll_projection_name_order_first must be one of ['first_name', 'last_name']",
response_message,
)
@@ -299,15 +299,14 @@ def test_update_poll_default_allow_invalid(self) -> None:
self.basic_test({"poll_default_allow_invalid": True})
self.assert_model_exists("meeting/1", {"poll_default_allow_invalid": True})
+ def test_update_poll_default_allow_vote_split(self) -> None:
+ self.basic_test({"poll_default_allow_vote_split": True})
+ self.assert_model_exists("meeting/1", {"poll_default_allow_vote_split": True})
+
def test_update_motions_block_slide_columns(self) -> None:
self.basic_test({"motions_block_slide_columns": 2})
self.assert_model_exists("meeting/1", {"motions_block_slide_columns": 2})
- def test_update_topic_poll_default_group(self) -> None:
- self.basic_test({"topic_poll_default_group_ids": [3]})
- self.assert_model_exists("meeting/1", {"topic_poll_default_group_ids": [3]})
- self.assert_model_exists("group/3", {"used_as_topic_poll_default_id": 1})
-
def test_update_only_one_time_1(self) -> None:
response_message = self.basic_test({"start_time": 150000}, check_200=False)
self.assertEqual(
diff --git a/tests/system/action/meeting_poll_default/__init__.py b/tests/system/action/meeting_poll_default/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/system/action/meeting_poll_default/test_create.py b/tests/system/action/meeting_poll_default/test_create.py
new file mode 100644
index 0000000000..09c3b93d4e
--- /dev/null
+++ b/tests/system/action/meeting_poll_default/test_create.py
@@ -0,0 +1,206 @@
+from openslides_backend.models.models import Poll
+from tests.system.action.base import BaseActionTestCase
+
+
+class MeetingPollDefaultCreateActionTest(BaseActionTestCase):
+ def setUp(self) -> None:
+ super().setUp()
+ self.create_meeting(17)
+
+ def test_create_simple_config_assignment(self) -> None:
+ response = self.request(
+ "meeting_poll_default.create",
+ {"meeting_id": 17, "used_as_assignment_poll_config_in_meeting_id": 17},
+ )
+ self.assert_status_code(response, 200)
+ self.assert_model_exists(
+ "meeting_poll_default/1",
+ {
+ "meeting_id": 17,
+ "used_as_assignment_poll_config_in_meeting_id": 17,
+ "sort_result_by_votes": True,
+ "allow_abstain": True,
+ "allow_nota": False,
+ "strike_out": False,
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID,
+ "visibility": Poll.VISIBILITY_SECRET,
+ "group_ids": None,
+ "display_chart": None,
+ },
+ )
+ self.assert_model_exists(
+ "meeting/17", {"poll_default_ids": [1], "assignment_poll_config_id": 1}
+ )
+
+ def test_create_simple_config_motion(self) -> None:
+ response = self.request(
+ "meeting_poll_default.create",
+ {"meeting_id": 17, "used_as_motion_poll_config_in_meeting_id": 17},
+ )
+ self.assert_status_code(response, 200)
+ self.assert_model_exists(
+ "meeting_poll_default/1",
+ {
+ "meeting_id": 17,
+ "used_as_motion_poll_config_in_meeting_id": 17,
+ "sort_result_by_votes": True,
+ "allow_abstain": True,
+ "allow_nota": False,
+ "strike_out": False,
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID,
+ "visibility": Poll.VISIBILITY_SECRET,
+ "group_ids": None,
+ "display_chart": None,
+ },
+ )
+ self.assert_model_exists(
+ "meeting/17", {"poll_default_ids": [1], "motion_poll_config_id": 1}
+ )
+
+ def test_create_simple_config_topic(self) -> None:
+ response = self.request(
+ "meeting_poll_default.create",
+ {"meeting_id": 17, "used_as_topic_poll_config_in_meeting_id": 17},
+ )
+ self.assert_status_code(response, 200)
+ self.assert_model_exists(
+ "meeting_poll_default/1",
+ {
+ "meeting_id": 17,
+ "used_as_topic_poll_config_in_meeting_id": 17,
+ "sort_result_by_votes": True,
+ "allow_abstain": True,
+ "allow_nota": False,
+ "strike_out": False,
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID,
+ "visibility": Poll.VISIBILITY_MANUALLY,
+ "group_ids": None,
+ "display_chart": "pie",
+ },
+ )
+ self.assert_model_exists(
+ "meeting/17", {"poll_default_ids": [1], "topic_poll_config_id": 1}
+ )
+
+ def test_create_set_all_fields(self) -> None:
+ data = {
+ "meeting_id": 17,
+ "used_as_topic_poll_config_in_meeting_id": 17,
+ "sort_result_by_votes": False,
+ "visibility": "named",
+ "allow_abstain": False,
+ "allow_nota": True,
+ "strike_out": True,
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_CAST,
+ "group_ids": [19],
+ "display_chart": "pie",
+ }
+ response = self.request("meeting_poll_default.create", data)
+ self.assert_status_code(response, 200)
+ self.assert_model_exists("meeting_poll_default/1", data)
+ self.assert_model_exists(
+ "meeting/17", {"poll_default_ids": [1], "topic_poll_config_id": 1}
+ )
+ self.assert_model_exists(
+ "group/19",
+ {"used_in_meeting_poll_default_ids": [1]},
+ )
+
+ def test_create_wrong_field(self) -> None:
+ response = self.request(
+ "meeting_poll_default.create",
+ {
+ "meeting_id": 17,
+ "used_as_motion_poll_config_in_meeting_id": 17,
+ "wrong_field": "test",
+ },
+ )
+
+ self.assert_status_code(response, 400)
+ self.assertIn(
+ "data must not contain {'wrong_field'} properties",
+ response.json["message"],
+ )
+ self.assert_model_not_exists("meeting_poll_default/1")
+
+ def test_create_empty_data(self) -> None:
+ response = self.request("meeting_poll_default.create", {})
+ self.assert_status_code(response, 400)
+ self.assertIn(
+ "data must contain ['meeting_id'] properties",
+ response.json["message"],
+ )
+ self.assert_model_not_exists("meeting_poll_default/1")
+
+ def test_create_no_poll_type_fields(self) -> None:
+ response = self.request("meeting_poll_default.create", {"meeting_id": 17})
+ self.assert_status_code(response, 400)
+ self.assertEqual(
+ "One of the fields ['used_as_assignment_poll_config_in_meeting_id', 'used_as_motion_poll_config_in_meeting_id', 'used_as_topic_poll_config_in_meeting_id'] must be set.",
+ response.json["message"],
+ )
+ self.assert_model_not_exists("meeting_poll_default/1")
+
+ def test_create_multiple_poll_type_fields(self) -> None:
+ response = self.request(
+ "meeting_poll_default.create",
+ {
+ "meeting_id": 17,
+ "used_as_assignment_poll_config_in_meeting_id": 17,
+ "used_as_topic_poll_config_in_meeting_id": 17,
+ },
+ )
+ self.assert_status_code(response, 400)
+ self.assertEqual(
+ "Only one of ['used_as_assignment_poll_config_in_meeting_id', 'used_as_motion_poll_config_in_meeting_id', 'used_as_topic_poll_config_in_meeting_id'] can be set.",
+ response.json["message"],
+ )
+ self.assert_model_not_exists("meeting_poll_default/1")
+
+ def test_create_meeting_id_mismatch(self) -> None:
+ self.create_meeting()
+ response = self.request(
+ "meeting_poll_default.create",
+ {"meeting_id": 17, "used_as_assignment_poll_config_in_meeting_id": 1},
+ )
+ self.assert_status_code(response, 400)
+ self.assertEqual(
+ "Values in fields 'used_as_assignment_poll_config_in_meeting_id' and 'meeting_id' don't match.",
+ response.json["message"],
+ )
+ self.assert_model_not_exists("meeting_poll_default/1")
+
+ def test_create_duplicate(self) -> None:
+ self.set_models(
+ {
+ "meeting_poll_default/21": {"meeting_id": 17},
+ "meeting/17": {"assignment_poll_config_id": 21},
+ }
+ )
+ response = self.request(
+ "meeting_poll_default.create",
+ {"meeting_id": 17, "used_as_assignment_poll_config_in_meeting_id": 17},
+ )
+ self.assert_status_code(response, 400)
+ self.assert_model_not_exists("meeting_poll_default/2")
+ self.assertEqual(
+ "'used_as_assignment_poll_config_in_meeting_id' already exists in meeting/17.",
+ response.json["message"],
+ )
+
+ def test_create_group_ids_not_in_meeting(self) -> None:
+ self.create_meeting()
+ response = self.request(
+ "meeting_poll_default.create",
+ {
+ "meeting_id": 17,
+ "used_as_assignment_poll_config_in_meeting_id": 17,
+ "group_ids": [1],
+ },
+ )
+ self.assert_status_code(response, 400)
+ self.assertEqual(
+ "The following models do not belong to meeting 17: ['group/1']",
+ response.json["message"],
+ )
+ self.assert_model_not_exists("meeting_poll_default/1")
diff --git a/tests/system/action/meeting_poll_default/test_update.py b/tests/system/action/meeting_poll_default/test_update.py
new file mode 100644
index 0000000000..cb7085c531
--- /dev/null
+++ b/tests/system/action/meeting_poll_default/test_update.py
@@ -0,0 +1,77 @@
+from tests.system.action.base import BaseActionTestCase
+
+
+class MeetingPollDefaultUpdateActionTest(BaseActionTestCase):
+ def setUp(self) -> None:
+ super().setUp()
+ self.create_meeting(17)
+ self.set_models(
+ {
+ "meeting_poll_default/21": {"meeting_id": 17},
+ "meeting/17": {"topic_poll_config_id": 21},
+ }
+ )
+
+ def test_update_correctly(self) -> None:
+ response = self.request(
+ "meeting_poll_default.update",
+ {"id": 21, "visibility": "named"},
+ )
+ self.assert_status_code(response, 200)
+ self.assert_model_exists("meeting_poll_default/21", {"visibility": "named"})
+
+ def test_update_erase_data(self) -> None:
+ data = {
+ "sort_result_by_votes": None,
+ "visibility": None,
+ "allow_abstain": None,
+ "allow_nota": None,
+ "strike_out": None,
+ "onehundred_percent_base": None,
+ "group_ids": None,
+ "display_chart": None,
+ }
+ response = self.request("meeting_poll_default.update", {"id": 21, **data})
+ self.assert_status_code(response, 200)
+ self.assert_model_exists("meeting_poll_default/21", data)
+
+ def test_update_wrong_id(self) -> None:
+ response = self.request(
+ "meeting_poll_default.update", {"id": 200, "allow_abstain": False}
+ )
+ self.assert_status_code(response, 400)
+ self.assertIn(
+ "Model 'meeting_poll_default/200' does not exist.",
+ response.json["message"],
+ )
+
+ def test_update_wrong_field(self) -> None:
+ response = self.request(
+ "meeting_poll_default.update",
+ {
+ "id": 21,
+ "allow_nota": True,
+ "wrong_id": "eleven",
+ },
+ )
+ self.assert_status_code(response, 400)
+ self.assert_model_exists("meeting_poll_default/21", {"allow_nota": False})
+ self.assertIn(
+ "data must not contain {'wrong_id'} properties", response.json["message"]
+ )
+
+ def test_update_group_ids_not_in_meeting(self) -> None:
+ self.create_meeting()
+ response = self.request(
+ "meeting_poll_default.update",
+ {
+ "id": 21,
+ "group_ids": [1],
+ },
+ )
+ self.assert_status_code(response, 400)
+ self.assertEqual(
+ "The following models do not belong to meeting 1: ['meeting_poll_default/21']",
+ response.json["message"],
+ )
+ self.assert_model_exists("meeting_poll_default/21", {"group_ids": None})
diff --git a/tests/system/presenter/test_check_database.py b/tests/system/presenter/test_check_database.py
index f6e4d37112..b4396de66e 100644
--- a/tests/system/presenter/test_check_database.py
+++ b/tests/system/presenter/test_check_database.py
@@ -83,12 +83,8 @@ def get_meeting_defaults(self) -> dict[str, Any]:
"motions_supporters_min_amount": 0,
"motions_export_submitter_recommendation": True,
"motions_export_follow_recommendation": False,
- "motion_poll_ballot_paper_selection": "CUSTOM_NUMBER",
- "motion_poll_ballot_paper_number": 8,
- "motion_poll_default_type": "pseudoanonymous",
- "motion_poll_default_onehundred_percent_base": "valid",
- "motion_poll_projection_name_order_first": "last_name",
- "motion_poll_projection_max_columns": 6,
+ "poll_projection_name_order_first": "last_name",
+ "poll_projection_max_columns": 6,
"users_enable_presence_view": False,
"users_enable_vote_weight": False,
"users_enable_vote_delegations": True,
@@ -99,16 +95,10 @@ def get_meeting_defaults(self) -> dict[str, Any]:
"users_email_subject": "OpenSlides access data",
"users_email_body": "blablabla",
"assignments_export_title": "Elections",
- "assignment_poll_ballot_paper_selection": "CUSTOM_NUMBER",
- "assignment_poll_ballot_paper_number": 8,
"assignment_poll_add_candidates_to_list_of_speakers": False,
- "assignment_poll_enable_max_votes_per_option": False,
- "assignment_poll_sort_poll_result_by_votes": True,
- "assignment_poll_default_type": "pseudoanonymous",
- "assignment_poll_default_method": "Y",
- "assignment_poll_default_onehundred_percent_base": "valid",
- "poll_default_type": "analog",
- "poll_default_onehundred_percent_base": "valid",
+ "assignment_poll_default_method": "selection",
+ "topic_poll_default_method": "selection",
+ "poll_enable_max_votes_per_option": False,
"poll_default_live_voting_enabled": False,
"poll_default_allow_invalid": False,
"poll_couple_countdown": True,
@@ -287,6 +277,9 @@ def test_correct_relations(self) -> None:
"is_archived_in_organization_id": 1,
"template_for_organization_id": 1,
"default_meeting_for_committee_id": 1,
+ "assignment_poll_config_id": 1,
+ "motion_poll_config_id": 2,
+ "topic_poll_config_id": 3,
"organization_tag_ids": [1],
"user_ids": [1, 2, 3, 4, 5, 6],
"present_user_ids": [2],
@@ -296,6 +289,7 @@ def test_correct_relations(self) -> None:
"font_bold_id": 2,
"font_regular_id": 4,
"meeting_user_ids": [11, 12, 13, 14, 15, 16],
+ "poll_default_ids": [1, 2, 3],
**{field: [1] for field in Meeting.all_default_projectors()},
**self.get_meeting_defaults(),
},
@@ -305,6 +299,7 @@ def test_correct_relations(self) -> None:
"weight": 1,
"default_group_for_meeting_id": 1,
"meeting_user_ids": [11, 12, 13, 14, 15, 16],
+ "used_in_meeting_poll_default_ids": [1, 2],
},
"group/2": {
"meeting_id": 1,
@@ -312,6 +307,7 @@ def test_correct_relations(self) -> None:
"weight": 1,
"admin_group_for_meeting_id": 1,
"meeting_mediafile_inherited_access_group_ids": [4],
+ "used_in_meeting_poll_default_ids": [1, 2, 3],
},
"user/1": {
"username": "no",
@@ -499,6 +495,32 @@ def test_correct_relations(self) -> None:
"content_object_id": "motion/1",
"meeting_id": 1,
},
+ "meeting_poll_default/1": {
+ "meeting_id": 1,
+ "used_as_assignment_poll_config_in_meeting_id": 1,
+ "group_ids": [1, 2],
+ "sort_result_by_votes": True,
+ "visibility": "secret",
+ "onehundred_percent_base": "valid",
+ "allow_abstain": False,
+ },
+ "meeting_poll_default/2": {
+ "meeting_id": 1,
+ "used_as_motion_poll_config_in_meeting_id": 1,
+ "group_ids": [1, 2],
+ "visibility": "secret",
+ "onehundred_percent_base": "valid",
+ "allow_abstain": True,
+ },
+ "meeting_poll_default/3": {
+ "meeting_id": 1,
+ "used_as_topic_poll_config_in_meeting_id": 1,
+ "group_ids": [2],
+ "sort_result_by_votes": True,
+ "visibility": "named",
+ "onehundred_percent_base": "valid",
+ "display_chart": "pie",
+ },
"poll/7": {
"title": "Poll 7",
"meeting_id": 1,
diff --git a/tests/system/presenter/test_check_database_all.py b/tests/system/presenter/test_check_database_all.py
index e5ede36915..d62a8a4af7 100644
--- a/tests/system/presenter/test_check_database_all.py
+++ b/tests/system/presenter/test_check_database_all.py
@@ -89,12 +89,8 @@ def get_meeting_defaults(self) -> dict[str, Any]:
"motions_supporters_min_amount": 0,
"motions_export_submitter_recommendation": True,
"motions_export_follow_recommendation": False,
- "motion_poll_ballot_paper_selection": "CUSTOM_NUMBER",
- "motion_poll_ballot_paper_number": 8,
- "motion_poll_default_type": "pseudoanonymous",
- "motion_poll_default_onehundred_percent_base": "valid",
- "motion_poll_projection_name_order_first": "last_name",
- "motion_poll_projection_max_columns": 6,
+ "poll_projection_name_order_first": "last_name",
+ "poll_projection_max_columns": 6,
"users_enable_presence_view": False,
"users_enable_vote_weight": False,
"users_enable_vote_delegations": True,
@@ -106,16 +102,10 @@ def get_meeting_defaults(self) -> dict[str, Any]:
"users_email_subject": "OpenSlides access data",
"users_email_body": "blablabla",
"assignments_export_title": "Elections",
- "assignment_poll_ballot_paper_selection": "CUSTOM_NUMBER",
- "assignment_poll_ballot_paper_number": 8,
"assignment_poll_add_candidates_to_list_of_speakers": False,
- "assignment_poll_enable_max_votes_per_option": False,
- "assignment_poll_sort_poll_result_by_votes": True,
- "assignment_poll_default_type": "pseudoanonymous",
- "assignment_poll_default_method": "Y",
- "assignment_poll_default_onehundred_percent_base": "valid",
- "poll_default_type": "analog",
- "poll_default_onehundred_percent_base": "valid",
+ "assignment_poll_default_method": "selection",
+ "topic_poll_default_method": "selection",
+ "poll_enable_max_votes_per_option": False,
"poll_default_live_voting_enabled": False,
"poll_default_allow_invalid": False,
"poll_couple_countdown": True,
@@ -383,6 +373,9 @@ def test_correct_relations(self) -> None:
"is_archived_in_organization_id": 1,
"template_for_organization_id": 1,
"default_meeting_for_committee_id": 1,
+ "assignment_poll_config_id": 1,
+ "motion_poll_config_id": 2,
+ "topic_poll_config_id": 3,
"organization_tag_ids": [1],
"user_ids": [1, 2, 3, 4, 5, 6],
"present_user_ids": [2],
@@ -391,6 +384,7 @@ def test_correct_relations(self) -> None:
"logo_web_header_id": 1,
"font_bold_id": 2,
"meeting_user_ids": [11, 12, 13, 14, 15, 16],
+ "poll_default_ids": [1, 2, 3],
**{field: [1] for field in Meeting.all_default_projectors()},
**self.get_meeting_defaults(),
},
@@ -401,12 +395,14 @@ def test_correct_relations(self) -> None:
"default_group_for_meeting_id": 1,
"meeting_user_ids": [11, 12, 13, 14, 15, 16],
"meeting_mediafile_access_group_ids": [4],
+ "used_in_meeting_poll_default_ids": [1, 2],
},
"group/2": {
"meeting_id": 1,
"name": "admin group",
"weight": 1,
"admin_group_for_meeting_id": 1,
+ "used_in_meeting_poll_default_ids": [1, 2, 3],
},
"user/1": {
"meeting_user_ids": [11],
@@ -575,6 +571,32 @@ def test_correct_relations(self) -> None:
"content_object_id": "motion/1",
"meeting_id": 1,
},
+ "meeting_poll_default/1": {
+ "meeting_id": 1,
+ "used_as_assignment_poll_config_in_meeting_id": 1,
+ "group_ids": [1, 2],
+ "sort_result_by_votes": True,
+ "visibility": "secret",
+ "onehundred_percent_base": "valid",
+ "allow_abstain": False,
+ },
+ "meeting_poll_default/2": {
+ "meeting_id": 1,
+ "used_as_motion_poll_config_in_meeting_id": 1,
+ "group_ids": [1, 2],
+ "visibility": "secret",
+ "onehundred_percent_base": "valid",
+ "allow_abstain": True,
+ },
+ "meeting_poll_default/3": {
+ "meeting_id": 1,
+ "used_as_topic_poll_config_in_meeting_id": 1,
+ "group_ids": [2],
+ "sort_result_by_votes": True,
+ "visibility": "named",
+ "onehundred_percent_base": "valid",
+ "display_chart": "pie",
+ },
"poll/7": {
"title": "Poll 1",
"meeting_id": 1,
diff --git a/tests/system/presenter/test_export_meeting.py b/tests/system/presenter/test_export_meeting.py
index 80ad98d989..b7ff6c871d 100644
--- a/tests/system/presenter/test_export_meeting.py
+++ b/tests/system/presenter/test_export_meeting.py
@@ -60,6 +60,7 @@ def test_correct(self) -> None:
"motion_category",
"motion_block",
"motion_change_recommendation",
+ "meeting_poll_default",
"poll",
"assignment",
"assignment_candidate",
@@ -302,6 +303,67 @@ def create_assignment(self, meeting_id: int, base: int) -> None:
}
)
+ def test_export_meeting_find_poll_defaults(self) -> None:
+ poll_defaults_data = {
+ "meeting_poll_default/1": {
+ "meeting_id": 1,
+ "used_as_assignment_poll_config_in_meeting_id": 1,
+ "group_ids": [2, 3],
+ "sort_result_by_votes": True,
+ "visibility": "open",
+ "onehundred_percent_base": "valid",
+ "allow_abstain": True,
+ "allow_nota": True,
+ },
+ "meeting_poll_default/2": {
+ "meeting_id": 1,
+ "used_as_motion_poll_config_in_meeting_id": 1,
+ "group_ids": [2, 3],
+ "sort_result_by_votes": False,
+ "visibility": "secret",
+ "onehundred_percent_base": "valid",
+ "allow_abstain": False,
+ "strike_out": True,
+ },
+ "meeting_poll_default/3": {
+ "meeting_id": 1,
+ "used_as_topic_poll_config_in_meeting_id": 1,
+ "group_ids": [3],
+ "sort_result_by_votes": True,
+ "visibility": "named",
+ "onehundred_percent_base": "valid",
+ "display_chart": "pie",
+ },
+ }
+ poll_default_config_ids = {
+ "assignment_poll_config_id": 1,
+ "motion_poll_config_id": 2,
+ "topic_poll_config_id": 3,
+ }
+ self.set_models(
+ {
+ "meeting/1": poll_default_config_ids,
+ "group/2": {"used_in_meeting_poll_default_ids": [1, 2]},
+ "group/3": {"used_in_meeting_poll_default_ids": [1, 2, 3]},
+ **poll_defaults_data,
+ }
+ )
+ status_code, data = self.request("export_meeting", {"meeting_id": 1})
+ assert status_code == 200
+ for fqid, poll_default_data in poll_defaults_data.items():
+ collection, id_ = fqid.split("/")
+ for k, v in poll_default_data.items():
+ assert data[collection][id_][k] == v
+ assert data["meeting"]["1"]["poll_default_ids"] == [1, 2, 3]
+ for field_name, config_id in poll_default_config_ids.items():
+ assert data["meeting"]["1"][field_name] == config_id
+ for group_id, config_ids in {2: [1, 2], 3: [1, 2, 3]}.items():
+ assert (
+ data["group"][str(group_id)]["used_in_meeting_poll_default_ids"]
+ == config_ids
+ )
+ assert "used_in_meeting_poll_default_ids" not in data["group"]["1"]
+
def test_export_meeting_find_poll_configs(self) -> None:
configs_data = {
"poll_config_approval/1": {
@@ -478,12 +540,8 @@ def test_with_structured_published_orga_files(self) -> None:
"motions_supporters_min_amount": 0,
"motions_export_submitter_recommendation": True,
"motions_export_follow_recommendation": False,
- "motion_poll_ballot_paper_selection": "CUSTOM_NUMBER",
- "motion_poll_ballot_paper_number": 8,
- "motion_poll_default_type": "pseudoanonymous",
- "motion_poll_default_onehundred_percent_base": "valid",
- "motion_poll_projection_name_order_first": "last_name",
- "motion_poll_projection_max_columns": 6,
+ "poll_projection_name_order_first": "last_name",
+ "poll_projection_max_columns": 6,
"users_enable_presence_view": False,
"users_enable_vote_weight": False,
"users_enable_vote_delegations": True,
@@ -494,18 +552,10 @@ def test_with_structured_published_orga_files(self) -> None:
"users_email_subject": "OpenSlides access data",
"users_email_body": "blablabla",
"assignments_export_title": "Elections",
- "assignment_poll_ballot_paper_selection": "CUSTOM_NUMBER",
- "assignment_poll_ballot_paper_number": 8,
"assignment_poll_add_candidates_to_list_of_speakers": False,
- "assignment_poll_enable_max_votes_per_option": False,
- "assignment_poll_sort_poll_result_by_votes": True,
- "assignment_poll_default_type": "pseudoanonymous",
- "assignment_poll_default_method": "Y",
- "assignment_poll_default_onehundred_percent_base": "valid",
- "poll_default_type": "analog",
- "poll_default_onehundred_percent_base": "valid",
- "poll_default_live_voting_enabled": False,
- "poll_default_method": "YNA",
+ "assignment_poll_default_method": "rating_approval",
+ "poll_enable_max_votes_per_option": False,
+ "topic_poll_default_method": "rating_approval",
"poll_couple_countdown": True,
},
"group/1": {
diff --git a/tests/unit/test_checker.py b/tests/unit/test_checker.py
index 3350264621..7d618402be 100644
--- a/tests/unit/test_checker.py
+++ b/tests/unit/test_checker.py
@@ -170,7 +170,7 @@ def setUp(self) -> None:
"default_projector_amendment_ids": [1],
"default_projector_assignment_ids": [1],
"default_projector_motion_ids": [1],
- "default_projector_poll_ids": [1],
+ "default_projector_topic_poll_ids": [1],
"motion_workflow_ids": [1],
"motion_state_ids": [1],
}
@@ -202,7 +202,7 @@ def setUp(self) -> None:
"used_as_default_projector_for_countdown_in_meeting_id": 1,
"used_as_default_projector_for_assignment_poll_in_meeting_id": 1,
"used_as_default_projector_for_motion_poll_in_meeting_id": 1,
- "used_as_default_projector_for_poll_in_meeting_id": 1,
+ "used_as_default_projector_for_topic_poll_in_meeting_id": 1,
}
},
"motion_workflow": {
From b6d0f37233d5b6adc47ec067c58fcc4686df2f0e Mon Sep 17 00:00:00 2001
From: Viktoriia Krasnovyd <114735598+vkrasnovyd@users.noreply.github.com>
Date: Tue, 14 Jul 2026 15:08:09 +0200
Subject: [PATCH 19/23] [vote] Delete polls in backend (#3661)
---
cli/generate_models.py | 14 +--
data/example-data.json | 30 +++++-
docs/Actions-Overview.md | 11 +++
docs/actions/poll.delete.md | 9 ++
docs/actions/poll_ballot.delete.md | 9 ++
docs/actions/poll_config_approval.delete.md | 9 ++
.../poll_config_rating_approval.delete.md | 9 ++
.../poll_config_rating_score.delete.md | 9 ++
docs/actions/poll_config_selection.delete.md | 9 ++
.../poll_config_stv_scottish.delete.md | 9 ++
docs/actions/poll_option.delete.md | 9 ++
meta | 2 +-
openslides_backend/action/actions/__init__.py | 8 ++
.../action/actions/poll/__init__.py | 1 +
.../action/actions/poll/delete.py | 15 +++
.../action/actions/poll_ballot/__init__.py | 1 +
.../action/actions/poll_ballot/delete.py | 15 +++
.../actions/poll_config_approval/__init__.py | 1 +
.../actions/poll_config_approval/delete.py | 15 +++
.../poll_config_rating_approval/__init__.py | 1 +
.../poll_config_rating_approval/delete.py | 17 ++++
.../poll_config_rating_score/__init__.py | 1 +
.../poll_config_rating_score/delete.py | 17 ++++
.../actions/poll_config_selection/__init__.py | 1 +
.../actions/poll_config_selection/delete.py | 17 ++++
.../poll_config_stv_scottish/__init__.py | 1 +
.../poll_config_stv_scottish/delete.py | 17 ++++
.../action/actions/poll_option/__init__.py | 1 +
.../action/actions/poll_option/delete.py | 15 +++
.../action/actions/user/base_merge_mixin.py | 3 +-
openslides_backend/action/generics/delete.py | 18 +---
openslides_backend/http/views/action_view.py | 6 --
openslides_backend/models/base.py | 3 -
openslides_backend/models/models.py | 54 +----------
openslides_backend/services/vote/__init__.py | 0
openslides_backend/services/vote/adapter.py | 91 -------------------
openslides_backend/services/vote/interface.py | 13 ---
.../shared/base_service_provider.py | 3 -
openslides_backend/shared/exceptions.py | 4 -
.../shared/interfaces/services.py | 3 -
openslides_backend/wsgi.py | 2 -
tests/system/action/meeting/test_clone.py | 33 +++----
tests/system/action/meeting/test_delete.py | 3 +-
tests/system/action/meeting/test_import.py | 2 +-
.../system/action/test_equal_fields_check.py | 2 +-
tests/system/base.py | 10 +-
tests/system/util.py | 5 -
47 files changed, 277 insertions(+), 251 deletions(-)
create mode 100644 docs/actions/poll.delete.md
create mode 100644 docs/actions/poll_ballot.delete.md
create mode 100644 docs/actions/poll_config_approval.delete.md
create mode 100644 docs/actions/poll_config_rating_approval.delete.md
create mode 100644 docs/actions/poll_config_rating_score.delete.md
create mode 100644 docs/actions/poll_config_selection.delete.md
create mode 100644 docs/actions/poll_config_stv_scottish.delete.md
create mode 100644 docs/actions/poll_option.delete.md
create mode 100644 openslides_backend/action/actions/poll/__init__.py
create mode 100644 openslides_backend/action/actions/poll/delete.py
create mode 100644 openslides_backend/action/actions/poll_ballot/__init__.py
create mode 100644 openslides_backend/action/actions/poll_ballot/delete.py
create mode 100644 openslides_backend/action/actions/poll_config_approval/__init__.py
create mode 100644 openslides_backend/action/actions/poll_config_approval/delete.py
create mode 100644 openslides_backend/action/actions/poll_config_rating_approval/__init__.py
create mode 100644 openslides_backend/action/actions/poll_config_rating_approval/delete.py
create mode 100644 openslides_backend/action/actions/poll_config_rating_score/__init__.py
create mode 100644 openslides_backend/action/actions/poll_config_rating_score/delete.py
create mode 100644 openslides_backend/action/actions/poll_config_selection/__init__.py
create mode 100644 openslides_backend/action/actions/poll_config_selection/delete.py
create mode 100644 openslides_backend/action/actions/poll_config_stv_scottish/__init__.py
create mode 100644 openslides_backend/action/actions/poll_config_stv_scottish/delete.py
create mode 100644 openslides_backend/action/actions/poll_option/__init__.py
create mode 100644 openslides_backend/action/actions/poll_option/delete.py
delete mode 100644 openslides_backend/services/vote/__init__.py
delete mode 100644 openslides_backend/services/vote/adapter.py
delete mode 100644 openslides_backend/services/vote/interface.py
diff --git a/cli/generate_models.py b/cli/generate_models.py
index 5e5b0d1d2f..a6cf71ff22 100644
--- a/cli/generate_models.py
+++ b/cli/generate_models.py
@@ -108,7 +108,7 @@ def main() -> None:
for collection, data in InternalHelper.MODELS.items():
if collection.startswith("_"):
continue
- model = Model(collection, data.get("managed_by"), data["fields"])
+ model = Model(collection, data["fields"])
dest.write(model.get_code())
if args.check:
@@ -130,26 +130,17 @@ class Node:
class Model(Node):
collection: str
- managed_by: str
attributes: dict[str, "Attribute"]
MODEL_TEMPLATE = string.Template(dedent("""
class ${class_name}(${base_classes}):
collection = "${collection}"
verbose_name = "${verbose_name}"
- managed_by = "${managed_by}"
"""))
- def __init__(
- self, collection: str, managed_by: str | None, fields: dict[str, dict[str, Any]]
- ) -> None:
+ def __init__(self, collection: str, fields: dict[str, dict[str, Any]]) -> None:
self.collection = collection
assert collection
- self.managed_by = managed_by or "backend"
- assert self.managed_by in [
- "backend",
- "vote",
- ], f"Forbidden value for {collection}/managed_by: '{managed_by}'. Available options are: 'backend', 'vote'."
self.attributes = {}
for field_name, field in fields.items():
if field_name == "_meta":
@@ -172,7 +163,6 @@ def get_code(self) -> str:
"base_classes": ", ".join(cls.__name__ for cls in base_classes),
"collection": self.collection,
"verbose_name": verbose_name,
- "managed_by": self.managed_by,
}
)
+ "\n"
diff --git a/data/example-data.json b/data/example-data.json
index 22d1e4cc14..6b037ed50e 100644
--- a/data/example-data.json
+++ b/data/example-data.json
@@ -528,7 +528,8 @@
2,
3,
4,
- 5
+ 5,
+ 6
],
"assignment_ids": [
1,
@@ -673,7 +674,7 @@
2
],
"poll_ids": [
- 5
+ 5, 6
],
"meeting_id": 1,
"used_in_meeting_poll_default_ids": [1, 2]
@@ -1991,6 +1992,21 @@
],
"entitled_group_ids": [2],
"meeting_id": 1
+ },
+ "6": {
+ "id": 6,
+ "title": "6",
+ "config_id": "poll_config_stv_scottish/1",
+ "visibility": "secret",
+ "state": "created",
+ "anonymized": false,
+ "live_voting_enabled": false,
+ "published": false,
+ "allow_invalid": false,
+ "allow_vote_split": true,
+ "content_object_id": "assignment/1",
+ "entitled_group_ids": [2],
+ "meeting_id": 1
}
},
"poll_config_approval": {
@@ -2040,6 +2056,13 @@
"onehundred_percent_base": "valid"
}
},
+ "poll_config_stv_scottish": {
+ "1": {
+ "id": 1,
+ "poll_id": 6,
+ "posts": 4
+ }
+ },
"poll_option": {
"1": {
"id": 1,
@@ -2174,7 +2197,8 @@
],
"poll_ids": [
2,
- 5
+ 5,
+ 6
],
"agenda_item_id": 11,
"list_of_speakers_id": 11,
diff --git a/docs/Actions-Overview.md b/docs/Actions-Overview.md
index 605d052f1f..09e272afa2 100644
--- a/docs/Actions-Overview.md
+++ b/docs/Actions-Overview.md
@@ -178,6 +178,17 @@ A more general format description see in [Action-Service](https://github.com/Ope
- [point_of_order_category.delete](actions/point_of_order_category.delete.md)
- [point_of_order_category.update](actions/point_of_order_category.update.md)
+## Poll related objects
+
+- [poll.delete](actions/poll.delete.md)
+- [poll_ballot.delete](actions/poll_ballot.delete.md)
+- [poll_config_approval.delete](actions/poll_config_approval.delete.md)
+- [poll_config_rating_approval.delete](actions/poll_config_rating_approval.delete.md)
+- [poll_config_rating_score.delete](actions/poll_config_rating_score.delete.md)
+- [poll_config_selection.delete](actions/poll_config_selection.delete.md)
+- [poll_config_stv_scottish.delete](actions/poll_config_stv_scottish.delete.md)
+- [poll_option.delete](actions/poll_option.delete.md)
+
## Speaker
- [speaker.create](actions/speaker.create.md)
diff --git a/docs/actions/poll.delete.md b/docs/actions/poll.delete.md
new file mode 100644
index 0000000000..bcde1c797b
--- /dev/null
+++ b/docs/actions/poll.delete.md
@@ -0,0 +1,9 @@
+## Payload
+
+```js
+{ id: Id; }
+```
+
+## Internal action
+
+Deletes a poll.
diff --git a/docs/actions/poll_ballot.delete.md b/docs/actions/poll_ballot.delete.md
new file mode 100644
index 0000000000..9574c2f761
--- /dev/null
+++ b/docs/actions/poll_ballot.delete.md
@@ -0,0 +1,9 @@
+## Payload
+
+```js
+{ id: Id; }
+```
+
+## Internal action
+
+Deletes a poll_ballot.
diff --git a/docs/actions/poll_config_approval.delete.md b/docs/actions/poll_config_approval.delete.md
new file mode 100644
index 0000000000..4a95f4ab5b
--- /dev/null
+++ b/docs/actions/poll_config_approval.delete.md
@@ -0,0 +1,9 @@
+## Payload
+
+```js
+{ id: Id; }
+```
+
+## Internal action
+
+Deletes a poll_config_approval.
diff --git a/docs/actions/poll_config_rating_approval.delete.md b/docs/actions/poll_config_rating_approval.delete.md
new file mode 100644
index 0000000000..28e868cd0a
--- /dev/null
+++ b/docs/actions/poll_config_rating_approval.delete.md
@@ -0,0 +1,9 @@
+## Payload
+
+```js
+{ id: Id; }
+```
+
+## Internal action
+
+Deletes a poll_config_rating_approval.
diff --git a/docs/actions/poll_config_rating_score.delete.md b/docs/actions/poll_config_rating_score.delete.md
new file mode 100644
index 0000000000..f4aeb1b4d5
--- /dev/null
+++ b/docs/actions/poll_config_rating_score.delete.md
@@ -0,0 +1,9 @@
+## Payload
+
+```js
+{ id: Id; }
+```
+
+## Internal action
+
+Deletes a poll_config_rating_score.
diff --git a/docs/actions/poll_config_selection.delete.md b/docs/actions/poll_config_selection.delete.md
new file mode 100644
index 0000000000..9dfc2a7695
--- /dev/null
+++ b/docs/actions/poll_config_selection.delete.md
@@ -0,0 +1,9 @@
+## Payload
+
+```js
+{ id: Id; }
+```
+
+## Internal action
+
+Deletes a poll_config_selection.
diff --git a/docs/actions/poll_config_stv_scottish.delete.md b/docs/actions/poll_config_stv_scottish.delete.md
new file mode 100644
index 0000000000..21e29f74c4
--- /dev/null
+++ b/docs/actions/poll_config_stv_scottish.delete.md
@@ -0,0 +1,9 @@
+## Payload
+
+```js
+{ id: Id; }
+```
+
+## Internal action
+
+Deletes a poll_config_stv_scottish.
diff --git a/docs/actions/poll_option.delete.md b/docs/actions/poll_option.delete.md
new file mode 100644
index 0000000000..0bdc8a82be
--- /dev/null
+++ b/docs/actions/poll_option.delete.md
@@ -0,0 +1,9 @@
+## Payload
+
+```js
+{ id: Id; }
+```
+
+## Internal action
+
+Deletes a poll_option.
diff --git a/meta b/meta
index 5f42bf2fec..64e82b5cc6 160000
--- a/meta
+++ b/meta
@@ -1 +1 @@
-Subproject commit 5f42bf2fecc78a4d2520d199ee45d273357fa525
+Subproject commit 64e82b5cc6ac00d7b43f30ce702f975a72435e09
diff --git a/openslides_backend/action/actions/__init__.py b/openslides_backend/action/actions/__init__.py
index 20ee012362..cc8ecbe8ed 100644
--- a/openslides_backend/action/actions/__init__.py
+++ b/openslides_backend/action/actions/__init__.py
@@ -38,6 +38,14 @@ def prepare_actions_map() -> None:
organization_tag,
personal_note,
point_of_order_category,
+ poll,
+ poll_ballot,
+ poll_config_approval,
+ poll_config_rating_approval,
+ poll_config_rating_score,
+ poll_config_selection,
+ poll_config_stv_scottish,
+ poll_option,
projection,
projector,
projector_countdown,
diff --git a/openslides_backend/action/actions/poll/__init__.py b/openslides_backend/action/actions/poll/__init__.py
new file mode 100644
index 0000000000..1fc925d0d8
--- /dev/null
+++ b/openslides_backend/action/actions/poll/__init__.py
@@ -0,0 +1 @@
+from . import delete # noqa
diff --git a/openslides_backend/action/actions/poll/delete.py b/openslides_backend/action/actions/poll/delete.py
new file mode 100644
index 0000000000..bfb4e42ff0
--- /dev/null
+++ b/openslides_backend/action/actions/poll/delete.py
@@ -0,0 +1,15 @@
+from ....models.models import Poll
+from ...generics.delete import DeleteAction
+from ...util.action_type import ActionType
+from ...util.default_schema import DefaultSchema
+from ...util.register import register_action
+
+
+@register_action("poll.delete", action_type=ActionType.BACKEND_INTERNAL)
+class PollDelete(DeleteAction):
+ """
+ Action to delete a poll.
+ """
+
+ model = Poll()
+ schema = DefaultSchema(Poll()).get_delete_schema()
diff --git a/openslides_backend/action/actions/poll_ballot/__init__.py b/openslides_backend/action/actions/poll_ballot/__init__.py
new file mode 100644
index 0000000000..1fc925d0d8
--- /dev/null
+++ b/openslides_backend/action/actions/poll_ballot/__init__.py
@@ -0,0 +1 @@
+from . import delete # noqa
diff --git a/openslides_backend/action/actions/poll_ballot/delete.py b/openslides_backend/action/actions/poll_ballot/delete.py
new file mode 100644
index 0000000000..13faf6f769
--- /dev/null
+++ b/openslides_backend/action/actions/poll_ballot/delete.py
@@ -0,0 +1,15 @@
+from ....models.models import PollBallot
+from ...generics.delete import DeleteAction
+from ...util.action_type import ActionType
+from ...util.default_schema import DefaultSchema
+from ...util.register import register_action
+
+
+@register_action("poll_ballot.delete", action_type=ActionType.BACKEND_INTERNAL)
+class PollBallotDelete(DeleteAction):
+ """
+ Action to delete a poll_ballot.
+ """
+
+ model = PollBallot()
+ schema = DefaultSchema(PollBallot()).get_delete_schema()
diff --git a/openslides_backend/action/actions/poll_config_approval/__init__.py b/openslides_backend/action/actions/poll_config_approval/__init__.py
new file mode 100644
index 0000000000..1fc925d0d8
--- /dev/null
+++ b/openslides_backend/action/actions/poll_config_approval/__init__.py
@@ -0,0 +1 @@
+from . import delete # noqa
diff --git a/openslides_backend/action/actions/poll_config_approval/delete.py b/openslides_backend/action/actions/poll_config_approval/delete.py
new file mode 100644
index 0000000000..a3c6301848
--- /dev/null
+++ b/openslides_backend/action/actions/poll_config_approval/delete.py
@@ -0,0 +1,15 @@
+from ....models.models import PollConfigApproval
+from ...generics.delete import DeleteAction
+from ...util.action_type import ActionType
+from ...util.default_schema import DefaultSchema
+from ...util.register import register_action
+
+
+@register_action("poll_config_approval.delete", action_type=ActionType.BACKEND_INTERNAL)
+class PollConfigApprovalDelete(DeleteAction):
+ """
+ Action to delete a poll_config_approval.
+ """
+
+ model = PollConfigApproval()
+ schema = DefaultSchema(PollConfigApproval()).get_delete_schema()
diff --git a/openslides_backend/action/actions/poll_config_rating_approval/__init__.py b/openslides_backend/action/actions/poll_config_rating_approval/__init__.py
new file mode 100644
index 0000000000..1fc925d0d8
--- /dev/null
+++ b/openslides_backend/action/actions/poll_config_rating_approval/__init__.py
@@ -0,0 +1 @@
+from . import delete # noqa
diff --git a/openslides_backend/action/actions/poll_config_rating_approval/delete.py b/openslides_backend/action/actions/poll_config_rating_approval/delete.py
new file mode 100644
index 0000000000..65c4600e8d
--- /dev/null
+++ b/openslides_backend/action/actions/poll_config_rating_approval/delete.py
@@ -0,0 +1,17 @@
+from ....models.models import PollConfigRatingApproval
+from ...generics.delete import DeleteAction
+from ...util.action_type import ActionType
+from ...util.default_schema import DefaultSchema
+from ...util.register import register_action
+
+
+@register_action(
+ "poll_config_rating_approval.delete", action_type=ActionType.BACKEND_INTERNAL
+)
+class PollConfigRatingApprovalDelete(DeleteAction):
+ """
+ Action to delete a poll_config_rating_approval.
+ """
+
+ model = PollConfigRatingApproval()
+ schema = DefaultSchema(PollConfigRatingApproval()).get_delete_schema()
diff --git a/openslides_backend/action/actions/poll_config_rating_score/__init__.py b/openslides_backend/action/actions/poll_config_rating_score/__init__.py
new file mode 100644
index 0000000000..1fc925d0d8
--- /dev/null
+++ b/openslides_backend/action/actions/poll_config_rating_score/__init__.py
@@ -0,0 +1 @@
+from . import delete # noqa
diff --git a/openslides_backend/action/actions/poll_config_rating_score/delete.py b/openslides_backend/action/actions/poll_config_rating_score/delete.py
new file mode 100644
index 0000000000..845590d404
--- /dev/null
+++ b/openslides_backend/action/actions/poll_config_rating_score/delete.py
@@ -0,0 +1,17 @@
+from ....models.models import PollConfigRatingScore
+from ...generics.delete import DeleteAction
+from ...util.action_type import ActionType
+from ...util.default_schema import DefaultSchema
+from ...util.register import register_action
+
+
+@register_action(
+ "poll_config_rating_score.delete", action_type=ActionType.BACKEND_INTERNAL
+)
+class PollConfigRatingScoreDelete(DeleteAction):
+ """
+ Action to delete a poll_config_rating_score.
+ """
+
+ model = PollConfigRatingScore()
+ schema = DefaultSchema(PollConfigRatingScore()).get_delete_schema()
diff --git a/openslides_backend/action/actions/poll_config_selection/__init__.py b/openslides_backend/action/actions/poll_config_selection/__init__.py
new file mode 100644
index 0000000000..1fc925d0d8
--- /dev/null
+++ b/openslides_backend/action/actions/poll_config_selection/__init__.py
@@ -0,0 +1 @@
+from . import delete # noqa
diff --git a/openslides_backend/action/actions/poll_config_selection/delete.py b/openslides_backend/action/actions/poll_config_selection/delete.py
new file mode 100644
index 0000000000..156b407a75
--- /dev/null
+++ b/openslides_backend/action/actions/poll_config_selection/delete.py
@@ -0,0 +1,17 @@
+from ....models.models import PollConfigSelection
+from ...generics.delete import DeleteAction
+from ...util.action_type import ActionType
+from ...util.default_schema import DefaultSchema
+from ...util.register import register_action
+
+
+@register_action(
+ "poll_config_selection.delete", action_type=ActionType.BACKEND_INTERNAL
+)
+class PollConfigSelectionDelete(DeleteAction):
+ """
+ Action to delete a poll_config_selection.
+ """
+
+ model = PollConfigSelection()
+ schema = DefaultSchema(PollConfigSelection()).get_delete_schema()
diff --git a/openslides_backend/action/actions/poll_config_stv_scottish/__init__.py b/openslides_backend/action/actions/poll_config_stv_scottish/__init__.py
new file mode 100644
index 0000000000..1fc925d0d8
--- /dev/null
+++ b/openslides_backend/action/actions/poll_config_stv_scottish/__init__.py
@@ -0,0 +1 @@
+from . import delete # noqa
diff --git a/openslides_backend/action/actions/poll_config_stv_scottish/delete.py b/openslides_backend/action/actions/poll_config_stv_scottish/delete.py
new file mode 100644
index 0000000000..0b92158515
--- /dev/null
+++ b/openslides_backend/action/actions/poll_config_stv_scottish/delete.py
@@ -0,0 +1,17 @@
+from ....models.models import PollConfigStvScottish
+from ...generics.delete import DeleteAction
+from ...util.action_type import ActionType
+from ...util.default_schema import DefaultSchema
+from ...util.register import register_action
+
+
+@register_action(
+ "poll_config_stv_scottish.delete", action_type=ActionType.BACKEND_INTERNAL
+)
+class PollConfigStvScottishDelete(DeleteAction):
+ """
+ Action to delete a poll_config_stv_scottish.
+ """
+
+ model = PollConfigStvScottish()
+ schema = DefaultSchema(PollConfigStvScottish()).get_delete_schema()
diff --git a/openslides_backend/action/actions/poll_option/__init__.py b/openslides_backend/action/actions/poll_option/__init__.py
new file mode 100644
index 0000000000..1fc925d0d8
--- /dev/null
+++ b/openslides_backend/action/actions/poll_option/__init__.py
@@ -0,0 +1 @@
+from . import delete # noqa
diff --git a/openslides_backend/action/actions/poll_option/delete.py b/openslides_backend/action/actions/poll_option/delete.py
new file mode 100644
index 0000000000..837629fea2
--- /dev/null
+++ b/openslides_backend/action/actions/poll_option/delete.py
@@ -0,0 +1,15 @@
+from ....models.models import PollOption
+from ...generics.delete import DeleteAction
+from ...util.action_type import ActionType
+from ...util.default_schema import DefaultSchema
+from ...util.register import register_action
+
+
+@register_action("poll_option.delete", action_type=ActionType.BACKEND_INTERNAL)
+class PollOptionDelete(DeleteAction):
+ """
+ Action to delete a poll_option.
+ """
+
+ model = PollOption()
+ schema = DefaultSchema(PollOption()).get_delete_schema()
diff --git a/openslides_backend/action/actions/user/base_merge_mixin.py b/openslides_backend/action/actions/user/base_merge_mixin.py
index fb56162a16..0d72a87d1f 100644
--- a/openslides_backend/action/actions/user/base_merge_mixin.py
+++ b/openslides_backend/action/actions/user/base_merge_mixin.py
@@ -98,8 +98,7 @@ def add_collection_field_groups(
self._all_collection_fields[collection] = [
i
for i in Class.__dict__.keys()
- if i[:1] != "_"
- and i not in ["collection", "verbose_name", "id", "managed_by"]
+ if i[:1] != "_" and i not in ["collection", "verbose_name", "id"]
]
if back_field:
self._collection_back_fields[collection] = back_field
diff --git a/openslides_backend/action/generics/delete.py b/openslides_backend/action/generics/delete.py
index bbb9b8d5d0..b5a39361de 100644
--- a/openslides_backend/action/generics/delete.py
+++ b/openslides_backend/action/generics/delete.py
@@ -1,7 +1,6 @@
from collections.abc import Iterable
from typing import Any, cast
-from ...models.base import collections_managed_by_vote
from ...models.fields import BaseRelationField, OnDelete
from ...shared.exceptions import ActionException, ProtectedModelsException
from ...shared.interfaces.event import Event, EventType
@@ -28,7 +27,6 @@ def base_update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
Takes care of on_delete handling.
"""
this_fqid = fqid_from_collection_and_id(self.model.collection, instance["id"])
- poll_ids_to_delete: list[int] = []
if self.datastore.is_to_be_deleted(this_fqid):
return instance
@@ -57,8 +55,7 @@ def base_update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
field = cast(BaseRelationField, self.model.get_field(field_name))
# Check on_delete.
# Extract all foreign keys as fqids from the model
- collection_from_field = field.get_target_collection()
- foreign_fqids = transform_to_fqids(value, collection_from_field)
+ foreign_fqids = transform_to_fqids(value, field.get_target_collection())
if field.on_delete != OnDelete.SET_NULL:
if field.on_delete == OnDelete.PROTECT:
protected_fqids = [
@@ -68,17 +65,6 @@ def base_update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
]
if protected_fqids:
raise ProtectedModelsException(this_fqid, protected_fqids)
- elif collection_from_field in collections_managed_by_vote:
- if collection_from_field == "poll":
- poll_ids_to_delete = [
- id_
- for id_ in value
- if not self.is_to_be_deleted(
- fqid_from_collection_and_id("poll", id_)
- )
- ]
- for fqid in foreign_fqids:
- self.datastore.apply_to_be_deleted(fqid)
else:
# case: field.on_delete == OnDelete.CASCADE
# Execute the delete action for all fqids
@@ -105,8 +91,6 @@ def base_update_instance(self, instance: dict[str, Any]) -> dict[str, Any]:
# Add additional relation models and execute all previously gathered delete actions
# catch all protected models exception to gather all protected fqids
all_protected_fqids: list[FullQualifiedId] = []
- for id_ in poll_ids_to_delete:
- self.vote_service.delete(id_)
for fqid, delete_action_class, delete_action_data in delete_actions:
try:
# Skip models that were deleted in the meantime
diff --git a/openslides_backend/http/views/action_view.py b/openslides_backend/http/views/action_view.py
index 72d2f57150..8db0e5303e 100644
--- a/openslides_backend/http/views/action_view.py
+++ b/openslides_backend/http/views/action_view.py
@@ -7,7 +7,6 @@
from ...i18n.translator import Translator
from ...migrations.migration_helper import MigrationHelper
from ...migrations.migration_manager import MigrationManager
-from ...services.auth.interface import AUTHENTICATION_HEADER, COOKIE_NAME
from ...services.postgresql.db_connection_handling import get_new_os_conn
from ...shared.env import DEV_PASSWORD
from ...shared.exceptions import AuthenticationException, ServerError
@@ -39,11 +38,6 @@ def action_route(self, request: Request) -> RouteResponse:
user_id, access_token = self.get_user_id_from_headers(
request.headers, request.cookies
)
- # Set Headers and Cookies in services.
- self.services.vote().set_authentication(
- request.headers.get(AUTHENTICATION_HEADER, ""),
- request.cookies.get(COOKIE_NAME, ""),
- )
# Handle request.
handler = ActionHandler(self.env, self.services, self.logging)
diff --git a/openslides_backend/models/base.py b/openslides_backend/models/base.py
index 3fc0d6ae0c..1c06fb1d83 100644
--- a/openslides_backend/models/base.py
+++ b/openslides_backend/models/base.py
@@ -10,7 +10,6 @@
from . import fields
model_registry: dict[Collection, type["Model"]] = {}
-collections_managed_by_vote: list[Collection] = []
def json_dict_to_non_json_data_types(json: dict[str, Any]) -> None:
@@ -60,8 +59,6 @@ def __new__(metaclass, class_name, class_parents, class_attributes): # type: ig
attr.own_collection = new_class.collection
attr.own_field_name = attr_name
model_registry[new_class.collection] = new_class
- if new_class.managed_by == "vote":
- collections_managed_by_vote.append(new_class.collection)
return new_class
diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py
index 266575c149..89dcb0880f 100644
--- a/openslides_backend/models/models.py
+++ b/openslides_backend/models/models.py
@@ -13,7 +13,6 @@
class ActionWorker(Model):
collection = "action_worker"
verbose_name = "action worker"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -35,7 +34,6 @@ class ActionWorker(Model):
class AgendaItem(Model, AgendaItemModelMixin):
collection = "agenda_item"
verbose_name = "agenda item"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
item_number = fields.CharField()
@@ -94,7 +92,6 @@ class AgendaItem(Model, AgendaItemModelMixin):
class Assignment(Model):
collection = "assignment"
verbose_name = "assignment"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
title = fields.CharField(required=True)
@@ -172,7 +169,6 @@ class Assignment(Model):
class AssignmentCandidate(Model):
collection = "assignment_candidate"
verbose_name = "assignment candidate"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
weight = fields.IntegerField(default=10000)
@@ -190,7 +186,6 @@ class AssignmentCandidate(Model):
class ChatGroup(Model):
collection = "chat_group"
verbose_name = "chat group"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -230,7 +225,6 @@ class ChatGroup(Model):
class ChatMessage(Model):
collection = "chat_message"
verbose_name = "chat message"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
content = fields.HTMLStrictField(required=True)
@@ -249,7 +243,6 @@ class ChatMessage(Model):
class Committee(Model):
collection = "committee"
verbose_name = "committee"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -344,7 +337,6 @@ class Committee(Model):
class Gender(Model):
collection = "gender"
verbose_name = "gender"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True, unique=True)
@@ -359,7 +351,6 @@ class Gender(Model):
class Group(Model):
collection = "group"
verbose_name = "group"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
external_id = fields.CharField()
@@ -535,7 +526,6 @@ class Group(Model):
class HistoryEntry(Model):
collection = "history_entry"
verbose_name = "history entry"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
entries = fields.TextArrayField()
@@ -558,7 +548,6 @@ class HistoryEntry(Model):
class HistoryPosition(Model):
collection = "history_position"
verbose_name = "history position"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
timestamp = fields.TimestampField(read_only=True)
@@ -574,7 +563,6 @@ class HistoryPosition(Model):
class ImportPreview(Model):
collection = "import_preview"
verbose_name = "import preview"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(
@@ -593,7 +581,6 @@ class ImportPreview(Model):
class ListOfSpeakers(Model):
collection = "list_of_speakers"
verbose_name = "list of speakers"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
closed = fields.BooleanField(default=False)
@@ -641,7 +628,6 @@ class ListOfSpeakers(Model):
class Mediafile(Model):
collection = "mediafile"
verbose_name = "mediafile"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
title = fields.CharField(
@@ -684,7 +670,6 @@ class Mediafile(Model):
class Meeting(Model, MeetingModelMixin):
collection = "meeting"
verbose_name = "meeting"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
external_id = fields.CharField(unique=True)
@@ -1333,7 +1318,6 @@ class Meeting(Model, MeetingModelMixin):
class MeetingMediafile(Model):
collection = "meeting_mediafile"
verbose_name = "meeting mediafile"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
mediafile_id = fields.RelationField(
@@ -1453,7 +1437,6 @@ class MeetingMediafile(Model):
class MeetingPollDefault(Model, MeetingPollDefaultModelMixin):
collection = "meeting_poll_default"
verbose_name = "meeting poll default"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
sort_result_by_votes = fields.BooleanField(default=True)
@@ -1492,7 +1475,6 @@ class MeetingPollDefault(Model, MeetingPollDefaultModelMixin):
class MeetingUser(Model):
collection = "meeting_user"
verbose_name = "meeting user"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
comment = fields.HTMLStrictField()
@@ -1585,7 +1567,6 @@ class MeetingUser(Model):
class Motion(Model):
collection = "motion"
verbose_name = "motion"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
number = fields.CharField()
@@ -1804,7 +1785,6 @@ class Motion(Model):
class MotionBlock(Model):
collection = "motion_block"
verbose_name = "motion block"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
title = fields.CharField(required=True)
@@ -1843,7 +1823,6 @@ class MotionBlock(Model):
class MotionCategory(Model):
collection = "motion_category"
verbose_name = "motion category"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -1876,7 +1855,6 @@ class MotionCategory(Model):
class MotionChangeRecommendation(Model):
collection = "motion_change_recommendation"
verbose_name = "motion change recommendation"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
rejected = fields.BooleanField(default=False)
@@ -1901,7 +1879,6 @@ class MotionChangeRecommendation(Model):
class MotionComment(Model):
collection = "motion_comment"
verbose_name = "motion comment"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
comment = fields.HTMLStrictField(required=True)
@@ -1919,7 +1896,6 @@ class MotionComment(Model):
class MotionCommentSection(Model):
collection = "motion_comment_section"
verbose_name = "motion comment section"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -1967,7 +1943,6 @@ class MotionCommentSection(Model):
class MotionEditor(Model):
collection = "motion_editor"
verbose_name = "motion editor"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
weight = fields.IntegerField()
@@ -1983,7 +1958,6 @@ class MotionEditor(Model):
class MotionState(Model):
collection = "motion_state"
verbose_name = "motion state"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -2070,7 +2044,6 @@ class MotionState(Model):
class MotionSubmitter(Model):
collection = "motion_submitter"
verbose_name = "motion submitter"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
weight = fields.IntegerField()
@@ -2086,7 +2059,6 @@ class MotionSubmitter(Model):
class MotionSupporter(Model):
collection = "motion_supporter"
verbose_name = "motion supporter"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
meeting_user_id = fields.RelationField(to={"meeting_user": "motion_supporter_ids"})
@@ -2101,7 +2073,6 @@ class MotionSupporter(Model):
class MotionWorkflow(Model):
collection = "motion_workflow"
verbose_name = "motion workflow"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -2136,7 +2107,6 @@ class MotionWorkflow(Model):
class MotionWorkingGroupSpeaker(Model):
collection = "motion_working_group_speaker"
verbose_name = "motion working group speaker"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
weight = fields.IntegerField()
@@ -2154,7 +2124,6 @@ class MotionWorkingGroupSpeaker(Model):
class Organization(Model):
collection = "organization"
verbose_name = "organization"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField()
@@ -2241,7 +2210,6 @@ class Organization(Model):
class OrganizationTag(Model):
collection = "organization_tag"
verbose_name = "organization tag"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -2268,7 +2236,6 @@ class OrganizationTag(Model):
class PersonalNote(Model):
collection = "personal_note"
verbose_name = "personal note"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
note = fields.HTMLStrictField()
@@ -2287,7 +2254,6 @@ class PersonalNote(Model):
class PointOfOrderCategory(Model):
collection = "point_of_order_category"
verbose_name = "point of order category"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
text = fields.CharField(required=True)
@@ -2303,7 +2269,6 @@ class PointOfOrderCategory(Model):
class Poll(Model, PollModelMixin):
collection = "poll"
verbose_name = "poll"
- managed_by = "vote"
id = fields.IntegerField(required=True, constant=True)
title = fields.CharField(required=True)
@@ -2315,6 +2280,7 @@ class Poll(Model, PollModelMixin):
"poll_config_selection": "poll_id",
"poll_config_approval": "poll_id",
},
+ on_delete=fields.OnDelete.CASCADE,
required=True,
)
option_ids = fields.RelationListField(
@@ -2407,7 +2373,6 @@ class Poll(Model, PollModelMixin):
class PollBallot(Model):
collection = "poll_ballot"
verbose_name = "poll ballot"
- managed_by = "vote"
id = fields.IntegerField(required=True, constant=True)
weight = fields.DecimalField(constant=True, default="1.000000")
@@ -2427,7 +2392,6 @@ class PollBallot(Model):
class PollConfigApproval(Model):
collection = "poll_config_approval"
verbose_name = "poll config approval"
- managed_by = "vote"
id = fields.IntegerField(required=True, constant=True)
poll_id = fields.RelationField(
@@ -2452,7 +2416,6 @@ class PollConfigApproval(Model):
class PollConfigRatingApproval(Model):
collection = "poll_config_rating_approval"
verbose_name = "poll config rating approval"
- managed_by = "vote"
id = fields.IntegerField(required=True, constant=True)
poll_id = fields.RelationField(
@@ -2480,7 +2443,6 @@ class PollConfigRatingApproval(Model):
class PollConfigRatingScore(Model):
collection = "poll_config_rating_score"
verbose_name = "poll config rating score"
- managed_by = "vote"
id = fields.IntegerField(required=True, constant=True)
poll_id = fields.RelationField(
@@ -2509,7 +2471,6 @@ class PollConfigRatingScore(Model):
class PollConfigSelection(Model):
collection = "poll_config_selection"
verbose_name = "poll config selection"
- managed_by = "vote"
id = fields.IntegerField(required=True, constant=True)
poll_id = fields.RelationField(
@@ -2538,7 +2499,6 @@ class PollConfigSelection(Model):
class PollConfigStvScottish(Model):
collection = "poll_config_stv_scottish"
verbose_name = "poll config stv scottish"
- managed_by = "vote"
id = fields.IntegerField(required=True, constant=True)
poll_id = fields.RelationField(
@@ -2550,7 +2510,6 @@ class PollConfigStvScottish(Model):
class PollOption(Model):
collection = "poll_option"
verbose_name = "poll option"
- managed_by = "vote"
id = fields.IntegerField(required=True, constant=True)
poll_id = fields.RelationField(to={"poll": "option_ids"}, required=True)
@@ -2562,7 +2521,6 @@ class PollOption(Model):
class Projection(Model):
collection = "projection"
verbose_name = "projection"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
options = fields.JSONField()
@@ -2603,7 +2561,6 @@ class Projection(Model):
class Projector(Model):
collection = "projector"
verbose_name = "projector"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -2705,7 +2662,6 @@ class Projector(Model):
class ProjectorCountdown(Model):
collection = "projector_countdown"
verbose_name = "projector countdown"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
title = fields.CharField(required=True)
@@ -2732,7 +2688,6 @@ class ProjectorCountdown(Model):
class ProjectorMessage(Model):
collection = "projector_message"
verbose_name = "projector message"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
message = fields.HTMLStrictField(required=True)
@@ -2749,7 +2704,6 @@ class ProjectorMessage(Model):
class Speaker(Model):
collection = "speaker"
verbose_name = "speaker"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
begin_time = fields.TimestampField()
@@ -2790,7 +2744,6 @@ class Speaker(Model):
class StructureLevel(Model):
collection = "structure_level"
verbose_name = "structure level"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -2818,7 +2771,6 @@ class StructureLevel(Model):
class StructureLevelListOfSpeakers(Model):
collection = "structure_level_list_of_speakers"
verbose_name = "structure level list of speakers"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
structure_level_id = fields.RelationField(
@@ -2863,7 +2815,6 @@ class StructureLevelListOfSpeakers(Model):
class Tag(Model):
collection = "tag"
verbose_name = "tag"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -2890,7 +2841,6 @@ class Tag(Model):
class Theme(Model):
collection = "theme"
verbose_name = "theme"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
name = fields.CharField(required=True)
@@ -2951,7 +2901,6 @@ class Theme(Model):
class Topic(Model):
collection = "topic"
verbose_name = "topic"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
title = fields.CharField(required=True)
@@ -3007,7 +2956,6 @@ class Topic(Model):
class User(Model):
collection = "user"
verbose_name = "user"
- managed_by = "backend"
id = fields.IntegerField(required=True, constant=True)
username = fields.CharField(required=True, unique=True)
diff --git a/openslides_backend/services/vote/__init__.py b/openslides_backend/services/vote/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/openslides_backend/services/vote/adapter.py b/openslides_backend/services/vote/adapter.py
deleted file mode 100644
index 7868ca596e..0000000000
--- a/openslides_backend/services/vote/adapter.py
+++ /dev/null
@@ -1,91 +0,0 @@
-from enum import StrEnum
-from typing import Any
-
-import requests
-import simplejson as json
-
-from ...shared.exceptions import VoteServiceException
-from ...shared.interfaces.logging import LoggingModule
-from ..shared.authenticated_service import AuthenticatedService
-from .interface import VoteService
-
-
-class RequestMethod(StrEnum):
- POST = "post"
- DELETE = "delete"
-
-
-class VoteAdapter(VoteService, AuthenticatedService):
- """
- Adapter to connect to the vote service.
- """
-
- def __init__(self, vote_url: str, logging: LoggingModule) -> None:
- self.url = vote_url
- self.logger = logging.getLogger(__name__)
-
- def retrieve(
- self,
- endpoint: str,
- request_method: RequestMethod = RequestMethod.POST,
- payload: dict[str, Any] | None = None,
- ) -> Any:
- response = self.make_request(endpoint, request_method, payload)
- message = f"Vote service sends HTTP {response.status_code} with the following content: {str(response.content)}."
- if response.status_code < 400:
- self.logger.debug(message)
- elif response.status_code == 500:
- self.logger.error(message)
- raise VoteServiceException(
- f"Vote service sends HTTP 500 Internal Server Error with the message: {message}."
- )
- else:
- self.logger.error(message)
- raise VoteServiceException(message)
- if response.content:
- return response.json()
-
- def make_request(
- self,
- endpoint: str,
- request_method: RequestMethod,
- payload: dict[str, Any] | None = None,
- ) -> Any:
- if not self.access_token or not self.refresh_id:
- raise VoteServiceException("You must be logged in to vote")
- payload_json = json.dumps(payload, separators=(",", ":")) if payload else None
- try:
- if request_method == RequestMethod.DELETE:
- return requests.delete(
- url=endpoint,
- headers={
- "Content-Type": "application/json",
- **self.get_auth_header(),
- },
- cookies=self.get_auth_cookie(),
- )
- return requests.post(
- url=endpoint,
- data=payload_json,
- headers={
- "Content-Type": "application/json",
- **self.get_auth_header(),
- },
- cookies=self.get_auth_cookie(),
- )
- except requests.exceptions.ConnectionError as e:
- self.logger.error(
- f"Cannot reach the vote service on {endpoint}. Error: {e}"
- )
- raise VoteServiceException(f"Cannot reach the vote service on {endpoint}.")
-
- def get_endpoint(self, id: int | None = None, route: str | None = None) -> str:
- return (
- f"{self.url}/poll"
- + (f"/{id}" if id else "")
- + (f"/{route}" if route else "")
- )
-
- def delete(self, id: int) -> dict[str, Any]:
- endpoint = self.get_endpoint(id)
- return self.retrieve(endpoint, RequestMethod.DELETE)
diff --git a/openslides_backend/services/vote/interface.py b/openslides_backend/services/vote/interface.py
deleted file mode 100644
index 7cc2844b30..0000000000
--- a/openslides_backend/services/vote/interface.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from abc import abstractmethod
-from typing import Any, Protocol
-
-from ..shared.authenticated_service import AuthenticatedServiceInterface
-
-
-class VoteService(AuthenticatedServiceInterface, Protocol):
- """
- Interface of the vote service.
- """
-
- @abstractmethod
- def delete(self, id: int) -> dict[str, Any]: ...
diff --git a/openslides_backend/shared/base_service_provider.py b/openslides_backend/shared/base_service_provider.py
index 908900b3c4..45ad03fb84 100644
--- a/openslides_backend/shared/base_service_provider.py
+++ b/openslides_backend/shared/base_service_provider.py
@@ -4,7 +4,6 @@
from openslides_backend.services.postgresql.db_connection_handling import (
get_current_os_conn_pool,
)
-from openslides_backend.services.vote.interface import VoteService
from openslides_backend.shared.interfaces.logging import Logger, LoggingModule
from openslides_backend.shared.interfaces.services import Services
@@ -18,7 +17,6 @@ class BaseServiceProvider:
datastore: Database
auth: AuthenticationService
media: MediaService
- vote: VoteService
logging: LoggingModule
logger: Logger
@@ -34,7 +32,6 @@ def __init__(
self.services = services
self.auth = services.authentication()
self.media = services.media()
- self.vote_service = services.vote()
self.datastore = datastore
self.logging = logging
os_conn_pool = get_current_os_conn_pool()
diff --git a/openslides_backend/shared/exceptions.py b/openslides_backend/shared/exceptions.py
index c1b95818e5..1316f9dafe 100644
--- a/openslides_backend/shared/exceptions.py
+++ b/openslides_backend/shared/exceptions.py
@@ -146,10 +146,6 @@ class MediaServiceException(ServiceException):
pass
-class VoteServiceException(ServiceException):
- pass
-
-
class PermissionDenied(ViewException):
status_code = 403
diff --git a/openslides_backend/shared/interfaces/services.py b/openslides_backend/shared/interfaces/services.py
index 44ba738560..b3746fe31f 100644
--- a/openslides_backend/shared/interfaces/services.py
+++ b/openslides_backend/shared/interfaces/services.py
@@ -3,7 +3,6 @@
from ...services.auth.interface import AuthenticationService
from ...services.database.interface import Database
from ...services.media.interface import MediaService
-from ...services.vote.interface import VoteService
class Services(Protocol):
@@ -16,5 +15,3 @@ def authentication(self) -> AuthenticationService: ...
def datastore(self) -> Database: ...
def media(self) -> MediaService: ...
-
- def vote(self) -> VoteService: ...
diff --git a/openslides_backend/wsgi.py b/openslides_backend/wsgi.py
index bf80c76022..2814efbddc 100644
--- a/openslides_backend/wsgi.py
+++ b/openslides_backend/wsgi.py
@@ -6,7 +6,6 @@
from .http.views import ActionView, PresenterView
from .services.auth.adapter import AuthenticationHTTPAdapter
from .services.media.adapter import MediaServiceAdapter
-from .services.vote.adapter import VoteAdapter
from .shared.interfaces.logging import LoggingModule
from .shared.interfaces.wsgi import View, WSGIApplication
@@ -21,7 +20,6 @@ class OpenSlidesBackendServices(containers.DeclarativeContainer):
env = providers.Object(0)
authentication = providers.Singleton(AuthenticationHTTPAdapter, logging)
media = providers.Singleton(MediaServiceAdapter, config.media_url, logging)
- vote = providers.Singleton(VoteAdapter, config.vote_url, logging)
class OpenSlidesBackendWSGI(containers.DeclarativeContainer):
diff --git a/tests/system/action/meeting/test_clone.py b/tests/system/action/meeting/test_clone.py
index bb35d1c54a..b924f43647 100644
--- a/tests/system/action/meeting/test_clone.py
+++ b/tests/system/action/meeting/test_clone.py
@@ -1773,29 +1773,22 @@ def test_clone_with_forwarded_motion(self) -> None:
},
"poll/1": {
"title": "a",
- "type": "pseudoanonymous",
- "backend": "fast",
- "pollmethod": "YNA",
- "state": "published",
- "onehundred_percent_base": "valid",
- "votesvalid": "1.000000",
- "votescast": "1.000000",
+ "visibility": Poll.VISIBILITY_SECRET,
+ "config_id": "poll_config_approval/1",
+ "state": Poll.STATE_FINISHED,
+ "published": True,
"content_object_id": "motion/1",
- "entitled_group_ids": [3],
"meeting_id": 1,
},
- "option/2": {
- "weight": 0,
- "poll_id": 1,
- "content_object_id": "motion/1",
- "meeting_id": 1,
+ "poll_config_approval/1": {
+ "onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID,
+ "allow_abstain": True,
},
- "vote/3": {
+ "group/3": {"poll_ids": [1]},
+ "poll_ballot/3": {
"weight": "0.000000",
"value": "Y",
- "user_token": "ABC",
- "option_id": 2,
- "meeting_id": 1,
+ "poll_id": 1,
},
"motion/2": {
"meeting_id": 4,
@@ -1858,13 +1851,11 @@ def test_clone_with_forwarded_motion(self) -> None:
{"comment": "", "motion_id": 5, "section_id": 3, "meeting_id": 5},
)
self.assert_model_exists(
- "vote/4",
+ "poll_ballot/4",
{
"weight": Decimal("0.000000"),
"value": "Y",
- "user_token": "ABC",
- "option_id": 3,
- "meeting_id": 5,
+ "poll_id": 2,
},
)
diff --git a/tests/system/action/meeting/test_delete.py b/tests/system/action/meeting/test_delete.py
index 7c19ab287e..d0a7795f16 100644
--- a/tests/system/action/meeting/test_delete.py
+++ b/tests/system/action/meeting/test_delete.py
@@ -439,7 +439,7 @@ def test_delete_full_meeting(self) -> None:
self.assert_model_not_exists(f"motion_workflow/{i+1}")
for i in range(3):
self.assert_model_not_exists(f"meeting_poll_default/{i+1}")
- for i in range(5):
+ for i in range(6):
self.assert_model_not_exists(f"poll/{i+1}")
for i in range(10):
self.assert_model_not_exists(f"poll_option/{i+1}")
@@ -450,6 +450,7 @@ def test_delete_full_meeting(self) -> None:
self.assert_model_not_exists("poll_config_selection/1")
self.assert_model_not_exists("poll_config_rating_score/1")
self.assert_model_not_exists("poll_config_rating_approval/1")
+ self.assert_model_not_exists("poll_config_stv_scottish/1")
for i in range(2):
self.assert_model_not_exists(f"assignment/{i+1}")
for i in range(5):
diff --git a/tests/system/action/meeting/test_import.py b/tests/system/action/meeting/test_import.py
index b19d851e8a..3d8d15efdb 100644
--- a/tests/system/action/meeting/test_import.py
+++ b/tests/system/action/meeting/test_import.py
@@ -3075,7 +3075,7 @@ def test_import_export_with_orga_mediafiles(self) -> None:
}
)
presenterapp = create_presenter_test_application()
- presenterclient = Client(presenterapp, self.update_vote_service_auth_data)
+ presenterclient = Client(presenterapp)
presenterclient.login("admin", "admin")
self.auth_data = deepcopy(presenterclient.auth_data)
response = presenterclient.post(
diff --git a/tests/system/action/test_equal_fields_check.py b/tests/system/action/test_equal_fields_check.py
index 6819a7943a..5235c5d49c 100644
--- a/tests/system/action/test_equal_fields_check.py
+++ b/tests/system/action/test_equal_fields_check.py
@@ -322,7 +322,7 @@ def build_model_class(
equal_field: str,
) -> None:
coll = get_collection_name(rel_type, collection, equal_field)
- ef_model = Model(coll, "backend", loaded_yml[coll]["fields"])
+ ef_model = Model(coll, loaded_yml[coll]["fields"])
field_name = generate_collection_field_name(
rel_type.split(":")[0 if coll_abbrev == "b" else 1], coll_abbrev == "c"
)[0]
diff --git a/tests/system/base.py b/tests/system/base.py
index 713c1dec3b..149271cd6a 100644
--- a/tests/system/base.py
+++ b/tests/system/base.py
@@ -26,7 +26,6 @@
from openslides_backend.services.postgresql.db_connection_handling import (
get_new_os_conn,
)
-from openslides_backend.services.vote.interface import VoteService
from openslides_backend.shared.env import Environment
from openslides_backend.shared.exceptions import (
ActionException,
@@ -61,7 +60,6 @@
class BaseSystemTestCase(TestCase):
app: OpenSlidesBackendWSGIApplication
auth: AuthenticationService
- vote_service: VoteService
media: Any # Any is needed because it is mocked and has magic methods
client: Client
anon_client: Client
@@ -83,7 +81,6 @@ def setUp(self) -> None:
self.env = cast(Environment, self.app.env)
self.auth = self.services.authentication()
self.media = self.services.media()
- self.vote_service = self.services.vote()
self.set_thread_watch_timeout(-1)
self.user_id = 1
@@ -102,7 +99,7 @@ def setUp(self) -> None:
},
}
)
- self.client = self.create_client(self.update_vote_service_auth_data)
+ self.client = self.create_client()
self.client.auth = self.auth # type: ignore
if self.init_with_login:
if self.auth_data:
@@ -191,11 +188,6 @@ def login(self, user_id: int) -> None:
assert user.get("default_password")
self.client.login(user["username"], user["default_password"])
- def update_vote_service_auth_data(self, auth_data: AuthData) -> None:
- self.vote_service.set_authentication(
- auth_data["access_token"], auth_data["refresh_id"]
- )
-
def get_application(self) -> OpenSlidesBackendWSGIApplication:
raise NotImplementedError()
diff --git a/tests/system/util.py b/tests/system/util.py
index a169287f45..a215b2c9c3 100644
--- a/tests/system/util.py
+++ b/tests/system/util.py
@@ -5,7 +5,6 @@
from unittest.mock import MagicMock, Mock, patch
import pytest
-from dependency_injector import providers
from requests.models import Response as RequestsResponse
from openslides_backend.http.application import OpenSlidesBackendWSGIApplication
@@ -13,7 +12,6 @@
from openslides_backend.http.views.base_view import ROUTE_OPTIONS_ATTR, RouteFunction
from openslides_backend.services.database.extended_database import ExtendedDatabase
from openslides_backend.services.media.interface import MediaService
-from openslides_backend.services.vote.adapter import VoteAdapter
from openslides_backend.shared.env import Environment, is_truthy
from openslides_backend.shared.exceptions import MediaServiceException
from openslides_backend.shared.interfaces.services import Services
@@ -59,9 +57,6 @@ def create_test_application(view: type[View]) -> OpenSlidesBackendWSGIApplicatio
config=env.get_service_url(),
logging=MagicMock(),
)
- services.vote = providers.Singleton(
- VoteAdapter, services.config.vote_url, MagicMock()
- )
mock_media_service = Mock(MediaService)
mock_media_service.upload_mediafile = Mock(
side_effect=side_effect_for_upload_method
From 7150b58800e86499438d288534fad58fe1a0ee49 Mon Sep 17 00:00:00 2001
From: Viktoriia Krasnovyd <114735598+vkrasnovyd@users.noreply.github.com>
Date: Tue, 14 Jul 2026 15:35:23 +0200
Subject: [PATCH 20/23] [vote] Update history_entry.changed_fields (#3665)
---
meta | 2 +-
openslides_backend/action/action.py | 3 +-
.../actions/meeting_user/base_delete.py | 35 +++-
.../actions/meeting_user/history_mixin.py | 57 ++++--
.../action/actions/meeting_user/update.py | 1 +
.../action/actions/user/merge_mixins.py | 1 +
.../action/actions/user/set_present.py | 40 ++--
openslides_backend/models/models.py | 4 +
openslides_backend/shared/history_events.py | 2 +-
tests/database/reader/system/test_filter.py | 1 +
tests/system/action/base.py | 2 +-
tests/system/action/user/test_create.py | 5 +-
.../action/user/test_delegation_history.py | 36 ++--
.../system/action/user/test_merge_together.py | 183 +++++++++++++++---
tests/system/action/user/test_set_present.py | 61 ++++--
tests/system/action/user/test_update.py | 162 +++++++++++++++-
16 files changed, 492 insertions(+), 103 deletions(-)
diff --git a/meta b/meta
index 64e82b5cc6..5dcd6aeb03 160000
--- a/meta
+++ b/meta
@@ -1 +1 @@
-Subproject commit 64e82b5cc6ac00d7b43f30ce702f975a72435e09
+Subproject commit 5dcd6aeb03b83cc2620fae92222c9197c5b5da02
diff --git a/openslides_backend/action/action.py b/openslides_backend/action/action.py
index 63a6d2f59d..f2d3e61689 100644
--- a/openslides_backend/action/action.py
+++ b/openslides_backend/action/action.py
@@ -779,7 +779,8 @@ def merge_history_informations(
b = {}
for fqid, information in b.items():
if fqid in a:
- a[fqid]["entries"].extend(information["entries"])
+ if entries := information.get("entries"):
+ a[fqid].setdefault("entries", list()).extend(entries)
if changed_fields := information.get("changed_fields"):
a[fqid].setdefault("changed_fields", dict()).update(changed_fields)
else:
diff --git a/openslides_backend/action/actions/meeting_user/base_delete.py b/openslides_backend/action/actions/meeting_user/base_delete.py
index da1b06eac5..af6465b1df 100644
--- a/openslides_backend/action/actions/meeting_user/base_delete.py
+++ b/openslides_backend/action/actions/meeting_user/base_delete.py
@@ -3,6 +3,7 @@
from openslides_backend.shared.typing import HistoryInformation
from ....models.models import MeetingUser
+from ....services.database.commands import GetManyRequest
from ...generics.delete import DeleteAction
from ...util.default_schema import DefaultSchema
@@ -16,15 +17,33 @@ class MeetingUserBaseDelete(DeleteAction):
schema = DefaultSchema(MeetingUser()).get_delete_schema()
def get_history_information(self) -> HistoryInformation | None:
- users = self.get_instances_with_fields(["user_id", "meeting_id"])
- return {
- fqid_from_collection_and_id(
- "user", user["user_id"]
- ): build_history_information_data(
+ information: HistoryInformation = {}
+ meeting_users = self.get_instances_with_fields(["user_id", "meeting_id"])
+ users = self.datastore.get_many(
+ [
+ GetManyRequest(
+ "user",
+ [mu["user_id"] for mu in meeting_users],
+ ["is_present_in_meeting_ids"],
+ )
+ ],
+ lock_result=False,
+ use_changed_models=False,
+ )["user"]
+
+ for meeting_user in meeting_users:
+ information[
+ fqid_from_collection_and_id("user", meeting_user["user_id"])
+ ] = build_history_information_data(
[
"Participant removed from meeting {}",
- fqid_from_collection_and_id("meeting", user["meeting_id"]),
+ fqid_from_collection_and_id("meeting", meeting_user["meeting_id"]),
],
)
- for user in users
- }
+ if meeting_user["meeting_id"] in users.get(meeting_user["user_id"], {}).get(
+ "is_present_in_meeting_ids", []
+ ):
+ information[
+ fqid_from_collection_and_id("meeting_user", meeting_user["id"])
+ ] = build_history_information_data(changed_fields={"is_present": False})
+ return information
diff --git a/openslides_backend/action/actions/meeting_user/history_mixin.py b/openslides_backend/action/actions/meeting_user/history_mixin.py
index 134c8e0e92..49fe694acc 100644
--- a/openslides_backend/action/actions/meeting_user/history_mixin.py
+++ b/openslides_backend/action/actions/meeting_user/history_mixin.py
@@ -14,7 +14,7 @@
class ActionHistoryInformationData(TypedDict):
- entries: list[tuple[str, ...]]
+ entries: NotRequired[list[tuple[str, ...]]]
changed_fields: NotRequired[dict[Field, Any]]
@@ -120,8 +120,8 @@ def add_updated_meeting_user_history_information(
db_instance: dict[str, Any],
information: ActionHistoryInformation,
) -> None:
- instance_entries: list[tuple[str, ...]] = []
- instance_changed_fields: dict[str, list[int]] = {}
+ user_entries: list[tuple[str, ...]] = []
+ meeting_user_changed_fields: dict[str, dict[str, list[int]]] = {}
user_id = db_instance["user_id"]
meeting_id = db_instance["meeting_id"]
@@ -134,7 +134,7 @@ def add_updated_meeting_user_history_information(
# meeting specific data
update_fields = ["structure_level_ids", "number", "vote_weight"]
if any(field in instance for field in update_fields):
- instance_entries.append(
+ user_entries.append(
(
"Participant data updated in meeting {}",
fqid_from_collection_and_id("meeting", meeting_id),
@@ -142,20 +142,23 @@ def add_updated_meeting_user_history_information(
)
self.handle_group_updates(
- instance_entries,
- instance_changed_fields,
+ user_entries,
+ meeting_user_changed_fields,
instance,
db_instance,
)
- self.handle_delegations(information, instance_entries, instance, db_instance)
+ self.handle_delegations(information, user_entries, instance, db_instance)
- if instance_entries or instance_changed_fields:
+ if user_entries:
self.add_entries_to_history_information(
information,
- instance_entries,
+ user_entries,
for_user_id=user_id,
- changed_fields=instance_changed_fields,
)
+ if meeting_user_changed_fields:
+ information[
+ fqid_from_collection_and_id("meeting_user", db_instance["id"])
+ ] = {"changed_fields": meeting_user_changed_fields}
def add_created_meeting_user_history_information(
self,
@@ -200,15 +203,26 @@ def add_created_meeting_user_history_information(
information,
instance_information,
for_user_id=db_instance["user_id"],
- changed_fields={
- "group_ids": self.get_changed_group_ids(db_instance["user_id"])
- },
)
+ if "group_ids" in instance:
+ changed_fields: dict[str, dict[str, list[int]] | bool] = {
+ "group_ids": {"added": instance["group_ids"]}
+ }
+ is_present_in_meeting_ids = self.datastore.get(
+ fqid_from_collection_and_id("user", db_instance["user_id"]),
+ ["is_present_in_meeting_ids"],
+ lock_result=False,
+ ).get("is_present_in_meeting_ids", [])
+ if db_instance["meeting_id"] in is_present_in_meeting_ids:
+ changed_fields["is_present"] = True
+ information[fqid_from_collection_and_id("meeting_user", instance["id"])] = {
+ "changed_fields": changed_fields
+ }
def add_entries_to_history_information(
self,
information: ActionHistoryInformation,
- entries: list[tuple[str, ...]],
+ entries: list[tuple[str, ...]] = [],
for_user_id: int | None = None,
for_meeting_user_id: int | None = None,
changed_fields: dict[Field, Any] | None = None,
@@ -225,7 +239,8 @@ def add_entries_to_history_information(
user_id = for_user_id
fqid = fqid_from_collection_and_id("user", user_id)
if fqid not in information:
- information[fqid] = {"entries": entries}
+ if entries:
+ information[fqid] = {"entries": entries}
if changed_fields:
information[fqid]["changed_fields"] = changed_fields
else:
@@ -266,7 +281,7 @@ def compose_history_string(
def handle_group_updates(
self,
entries: list[tuple[str, ...]],
- changed_fields: dict[str, list[int]],
+ meeting_user_changed_fields: dict[str, dict[str, list[int]]],
instance: dict[str, Any],
db_instance: dict[str, Any],
) -> None:
@@ -277,9 +292,12 @@ def handle_group_updates(
added = instance_group_ids - db_group_ids
removed = db_group_ids - instance_group_ids
- if added or removed:
- changed_fields["group_ids"] = self.get_changed_group_ids(
- db_instance["user_id"], {meeting_id: instance["group_ids"]}
+ # Calculate history information for meeting_user
+ if added:
+ meeting_user_changed_fields["group_ids"] = {"added": list(added)}
+ if removed:
+ meeting_user_changed_fields.setdefault("group_ids", dict()).update(
+ {"removed": list(removed)}
)
# remove default groups
@@ -292,6 +310,7 @@ def handle_group_updates(
removed.discard(meeting.get("default_group_id"))
changed = added | removed
+ # Calculate history information for user
group_information: list[str] = []
if added and removed:
group_information.append("Groups changed")
diff --git a/openslides_backend/action/actions/meeting_user/update.py b/openslides_backend/action/actions/meeting_user/update.py
index f1703a83a2..a3ef6a3438 100644
--- a/openslides_backend/action/actions/meeting_user/update.py
+++ b/openslides_backend/action/actions/meeting_user/update.py
@@ -28,6 +28,7 @@ class MeetingUserUpdate(
merge_fields = [
"assignment_candidate_ids",
+ "history_entry_ids",
"motion_working_group_speaker_ids",
"motion_editor_ids",
"motion_supporter_ids",
diff --git a/openslides_backend/action/actions/user/merge_mixins.py b/openslides_backend/action/actions/user/merge_mixins.py
index ee260e07bb..5b5f7bea20 100644
--- a/openslides_backend/action/actions/user/merge_mixins.py
+++ b/openslides_backend/action/actions/user/merge_mixins.py
@@ -229,6 +229,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
"chat_message_ids",
"group_ids",
"structure_level_ids",
+ "history_entry_ids",
"poll_voted_ids", # throw error if conflict on same poll
"poll_option_ids", # throw error if conflict on same poll
"acting_ballot_ids", # throw error if conflict on same poll
diff --git a/openslides_backend/action/actions/user/set_present.py b/openslides_backend/action/actions/user/set_present.py
index 33473fa54c..0e3e044d16 100644
--- a/openslides_backend/action/actions/user/set_present.py
+++ b/openslides_backend/action/actions/user/set_present.py
@@ -4,6 +4,7 @@
from openslides_backend.shared.typing import HistoryInformation
from ....action.mixins.archived_meeting_check_mixin import CheckForArchivedMeetingMixin
+from ....action.mixins.meeting_user_helper import get_meeting_user
from ....models.models import User
from ....permissions.management_levels import OrganizationManagementLevel
from ....permissions.permission_helper import (
@@ -47,11 +48,12 @@ def get_updated_instances(self, action_data: ActionData) -> ActionData:
present = instance.pop("present")
user = self.datastore.get(
fqid_from_collection_and_id(self.model.collection, instance["id"]),
- ["is_present_in_meeting_ids"],
+ ["is_present_in_meeting_ids", "meeting_ids"],
)
self.base_history_information[instance["id"]] = {
"present": present,
"meeting_id": meeting_id,
+ "all_meeting_ids": user.get("meeting_ids", []),
}
if present:
if meeting_id not in user.get("is_present_in_meeting_ids", []):
@@ -106,15 +108,29 @@ def check_permissions(self, instance: dict[str, Any]) -> None:
raise PermissionDenied("You are not allowed to set present.")
def get_history_information(self) -> HistoryInformation | None:
- return {
- fqid_from_collection_and_id(
- self.model.collection, id_
- ): build_history_information_data(
- [
- f"Set {'not ' if not data['present'] else ''}present in meeting {{}}",
- fqid_from_collection_and_id("meeting", data["meeting_id"]),
- ],
- {"is_present_in_meeting_ids": data["is_present_in_meeting_ids"]},
+ information: HistoryInformation = {}
+ for user_id, data in self.base_history_information.items():
+ present = data["present"]
+ meeting_id = data["meeting_id"]
+ information[fqid_from_collection_and_id(self.model.collection, user_id)] = (
+ build_history_information_data(
+ [
+ f"Set {'not ' if not present else ''}present in meeting {{}}",
+ fqid_from_collection_and_id("meeting", meeting_id),
+ ],
+ )
)
- for id_, data in self.base_history_information.items()
- }
+ if meeting_id in data["all_meeting_ids"]:
+ meeting_user = get_meeting_user(
+ self.datastore,
+ meeting_id,
+ user_id,
+ ["id"],
+ )
+ if meeting_user:
+ information[
+ fqid_from_collection_and_id("meeting_user", meeting_user["id"])
+ ] = build_history_information_data(
+ changed_fields={"is_present": present}
+ )
+ return information
diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py
index 89dcb0880f..57aa2322e5 100644
--- a/openslides_backend/models/models.py
+++ b/openslides_backend/models/models.py
@@ -534,6 +534,7 @@ class HistoryEntry(Model):
model_id = fields.GenericRelationField(
to={
"assignment": "history_entry_ids",
+ "meeting_user": "history_entry_ids",
"motion": "history_entry_ids",
"poll": "history_entry_ids",
"user": "history_entry_ids",
@@ -1562,6 +1563,9 @@ class MeetingUser(Model):
[],
),
)
+ history_entry_ids = fields.RelationListField(
+ to={"history_entry": "model_id"}, is_view_field=True
+ )
class Motion(Model):
diff --git a/openslides_backend/shared/history_events.py b/openslides_backend/shared/history_events.py
index 0b980d210a..796b35e9b3 100644
--- a/openslides_backend/shared/history_events.py
+++ b/openslides_backend/shared/history_events.py
@@ -67,7 +67,7 @@ def calculate_history_event_payloads(
(
model_fqid_to_entry_id[fqid],
fqid,
- data["entries"],
+ data.get("entries") or None,
data.get("changed_fields"),
)
for fqid, data in information.items()
diff --git a/tests/database/reader/system/test_filter.py b/tests/database/reader/system/test_filter.py
index 4a2a7f2ab3..e3704b671b 100644
--- a/tests/database/reader/system/test_filter.py
+++ b/tests/database/reader/system/test_filter.py
@@ -180,6 +180,7 @@ def test_types_str_list(db_connection: Connection) -> None:
"meeting_id": 1,
"model_id": "user/1",
"model_id_assignment_id": None,
+ "model_id_meeting_user_id": None,
"model_id_motion_id": None,
"model_id_poll_id": None,
"model_id_user_id": 1,
diff --git a/tests/system/action/base.py b/tests/system/action/base.py
index a8a3bc475c..e7cb6cc4bb 100644
--- a/tests/system/action/base.py
+++ b/tests/system/action/base.py
@@ -319,7 +319,7 @@ def get_last_history_information(
def assert_history_information(
self,
fqid: FullQualifiedId,
- entries: list[str] | None,
+ entries: list[str] | None = None,
changed_fields: dict[str, Any] | None = None,
) -> None:
"""
diff --git a/tests/system/action/user/test_create.py b/tests/system/action/user/test_create.py
index a4208090f9..d4af5c602c 100644
--- a/tests/system/action/user/test_create.py
+++ b/tests/system/action/user/test_create.py
@@ -133,7 +133,10 @@ def test_create_some_more_fields(self) -> None:
"group/114",
"meeting/114",
],
- {"group_ids": [114]},
+ )
+ self.assert_history_information(
+ "meeting_user/1",
+ changed_fields={"group_ids": {"added": [114]}},
)
def test_create_comment(self) -> None:
diff --git a/tests/system/action/user/test_delegation_history.py b/tests/system/action/user/test_delegation_history.py
index b491b0f988..a5f3531475 100644
--- a/tests/system/action/user/test_delegation_history.py
+++ b/tests/system/action/user/test_delegation_history.py
@@ -19,6 +19,7 @@ def setUp(self) -> None:
)
self.colin_meeting_user_id = meeting_user_ids.pop(0)
self.next_user_id = self.colin_id + 1
+ self.next_meeting_user_id = self.colin_meeting_user_id + 1
def setup_delegation(self) -> None:
self.set_models(
@@ -44,8 +45,6 @@ def assert_delegated_to(
to_id: int,
prepend_to: list[str] = [],
prepend_from: list[str] = [],
- changed_fields_to: dict[str, list[int]] | None = None,
- changed_fields_from: dict[str, list[int]] | None = None,
) -> None:
self.assert_history_information(
f"user/{to_id}",
@@ -55,7 +54,6 @@ def assert_delegated_to(
f"user/{from_id}",
"meeting/1",
],
- changed_fields_to,
)
self.assert_history_information(
f"user/{from_id}",
@@ -65,15 +63,9 @@ def assert_delegated_to(
f"user/{to_id}",
"meeting/1",
],
- changed_fields_from,
)
- def assert_alice_redelegated_to(
- self,
- who_id: int,
- prepend: list[str] = [],
- changed_fields: dict[str, list[int]] | None = None,
- ) -> None:
+ def assert_alice_redelegated_to(self, who_id: int, prepend: list[str] = []) -> None:
self.assert_history_information(
f"user/{who_id}",
[
@@ -82,7 +74,6 @@ def assert_alice_redelegated_to(
f"user/{self.alice_id}",
"meeting/1",
],
- changed_fields,
)
self.assert_history_information(
f"user/{self.alice_id}",
@@ -103,6 +94,16 @@ def assert_alice_redelegated_to(
],
)
+ def assert_meeting_user_changed_fields(self, user_id: int) -> None:
+ self.assert_model_exists(
+ f"meeting_user/{self.next_meeting_user_id}",
+ {"user_id": user_id, "meeting_id": 1, "group_ids": [3]},
+ )
+ self.assert_history_information(
+ f"meeting_user/{self.next_meeting_user_id}",
+ changed_fields={"group_ids": {"added": [3]}},
+ )
+
def test_update_delegate_vote(self) -> None:
self.make_request({"vote_delegated_to_id": self.bob_id - 1}, self.alice_id)
self.assert_delegated_to(self.alice_id, self.bob_id)
@@ -126,8 +127,8 @@ def test_create_delegate_vote(self) -> None:
"group/3",
"meeting/1",
],
- changed_fields_from={"group_ids": [3]},
)
+ self.assert_meeting_user_changed_fields(self.next_user_id)
def test_create_receive_delegated_vote(self) -> None:
self.make_request(
@@ -144,8 +145,8 @@ def test_create_receive_delegated_vote(self) -> None:
"group/3",
"meeting/1",
],
- changed_fields_to={"group_ids": [3]},
)
+ self.assert_meeting_user_changed_fields(self.next_user_id)
def test_update_re_delegate_vote(self) -> None:
self.setup_delegation()
@@ -174,8 +175,8 @@ def test_create_re_delegate_vote_reverse(self) -> None:
"group/3",
"meeting/1",
],
- changed_fields={"group_ids": [3]},
)
+ self.assert_meeting_user_changed_fields(self.next_user_id)
def test_update_re_delegate_received_votes(self) -> None:
self.setup_delegation()
@@ -347,6 +348,7 @@ def test_update_multiple_from_ids(self) -> None:
def test_create_multiple_from_ids(self) -> None:
eric_id = self.create_user("eric", [3])
fredric_id = self.create_user("fredric", [3])
+ self.next_meeting_user_id += 2
self.make_request(
{
"vote_delegations_from_ids": [
@@ -384,8 +386,8 @@ def test_create_multiple_from_ids(self) -> None:
],
"meeting/1",
],
- {"group_ids": [3]},
)
+ self.assert_meeting_user_changed_fields(self.next_user_id + 2)
for id_ in [self.alice_id, self.bob_id, self.colin_id, eric_id, fredric_id]:
self.assert_history_information(
f"user/{id_}",
@@ -418,8 +420,8 @@ def test_update_create_meeting_user_receiving_delegation(self) -> None:
f"user/{self.alice_id}",
"meeting/1",
],
- {"group_ids": [3]},
)
+ self.assert_meeting_user_changed_fields(debra_id)
def test_update_create_meeting_user_with_delegation(self) -> None:
debra_id = self.create_user("debra")
@@ -446,5 +448,5 @@ def test_update_create_meeting_user_with_delegation(self) -> None:
f"user/{self.alice_id}",
"meeting/1",
],
- {"group_ids": [3]},
)
+ self.assert_meeting_user_changed_fields(debra_id)
diff --git a/tests/system/action/user/test_merge_together.py b/tests/system/action/user/test_merge_together.py
index c55de8b324..b8e6159507 100644
--- a/tests/system/action/user/test_merge_together.py
+++ b/tests/system/action/user/test_merge_together.py
@@ -5,6 +5,8 @@
from typing import Any, Literal, cast
from zoneinfo import ZoneInfo
+from psycopg.types.json import Jsonb
+
from openslides_backend.action.actions.speaker.speech_state import SpeechState
from openslides_backend.action.relations.relation_manager import RelationManager
from openslides_backend.action.util.actions_map import actions_map
@@ -487,44 +489,110 @@ def setup_complex_user_fields(self) -> None:
"original_model_id": "user/2",
"model_id": "user/2",
"position_id": 1,
- "entries": ["User created", "User added to meetings"],
+ "entries": [
+ "User created",
+ "User added to meetings",
+ "User added to groups 1, 2",
+ ],
},
"history_entry/2": {
+ "original_model_id": "meeting_user/12",
+ "model_id": "meeting_user/12",
+ "position_id": 1,
+ "changed_fields": Jsonb({"group_ids": {"added": [1, 2]}}),
+ },
+ "history_entry/3": {
"original_model_id": "user/3",
"model_id": "user/3",
"position_id": 1,
"entries": ["User created"],
},
- "history_entry/3": {
+ "history_entry/4": {
"original_model_id": "user/4",
"model_id": "user/4",
"position_id": 1,
- "entries": ["User created", "User added to meetings"],
+ "entries": [
+ "User created",
+ "User added to meetings",
+ "User added to groups 2, 4, 9",
+ ],
+ },
+ "history_entry/5": {
+ "original_model_id": "meeting_user/14",
+ "model_id": "meeting_user/14",
+ "position_id": 1,
+ "changed_fields": Jsonb({"group_ids": {"added": [2]}}),
+ },
+ "history_entry/6": {
+ "original_model_id": "meeting_user/44",
+ "model_id": "meeting_user/44",
+ "position_id": 1,
+ "changed_fields": Jsonb({"group_ids": {"added": [4]}}),
+ },
+ "history_entry/7": {
+ "original_model_id": "meeting_user/74",
+ "model_id": "meeting_user/74",
+ "position_id": 1,
+ "changed_fields": Jsonb({"group_ids": {"added": [9]}}),
},
"history_position/2": {
"timestamp": datetime.fromtimestamp(200000, ZoneInfo("UTC")),
"original_user_id": 4,
"user_id": 4,
},
- "history_entry/4": {
+ "history_entry/8": {
"original_model_id": "user/2",
"model_id": "user/2",
"position_id": 2,
- "entries": ["User added to meetings"],
+ "entries": [
+ "User added to meetings",
+ "User added to group 5",
+ ],
},
- "history_entry/5": {
+ "history_entry/9": {
+ "original_model_id": "meeting_user/42",
+ "model_id": "meeting_user/42",
+ "position_id": 2,
+ "changed_fields": Jsonb({"group_ids": {"added": [5]}}),
+ },
+ "history_entry/10": {
"original_model_id": "user/3",
"model_id": "user/3",
"position_id": 2,
- "entries": ["User added to meetings"],
+ "entries": [
+ "User added to meetings",
+ "User added to groups 4, 5, 8",
+ ],
},
- "history_entry/6": {
+ "history_entry/11": {
+ "original_model_id": "meeting_user/43",
+ "model_id": "meeting_user/43",
+ "position_id": 2,
+ "changed_fields": Jsonb({"group_ids": {"added": [4, 5]}}),
+ },
+ "history_entry/12": {
+ "original_model_id": "meeting_user/73",
+ "model_id": "meeting_user/73",
+ "position_id": 2,
+ "changed_fields": Jsonb({"group_ids": {"added": [8]}}),
+ },
+ "history_entry/13": {
"original_model_id": "user/5",
"model_id": "user/5",
"position_id": 2,
- "entries": ["User created", "User added to meetings"],
+ "entries": [
+ "User created",
+ "User added to meetings",
+ "User added to group 10",
+ ],
},
- "history_entry/7": {
+ "history_entry/14": {
+ "original_model_id": "meeting_user/105",
+ "model_id": "meeting_user/105",
+ "position_id": 2,
+ "changed_fields": Jsonb({"group_ids": {"added": [10]}}),
+ },
+ "history_entry/15": {
"original_model_id": "user/6",
"model_id": "user/6",
"position_id": 2,
@@ -551,6 +619,7 @@ def test_merge_with_user_fields(self) -> None:
"organization_id": 1,
"default_password": "user2",
"meeting_user_ids": [12, 42, 106, 107],
+ "history_entry_ids": [1, 3, 4, 8, 10, 13, 15, 16],
"password": password,
"pronoun": "he",
"first_name": "Nick",
@@ -583,6 +652,7 @@ def test_merge_with_user_fields(self) -> None:
"about_me": "I am an enthusiastic explorer",
"comment": "Nicks everything",
"number": "NOMNOM",
+ "history_entry_ids": [2, 5],
},
)
self.assert_model_exists(
@@ -593,6 +663,7 @@ def test_merge_with_user_fields(self) -> None:
"number": "num?",
"vote_weight": Decimal("2"),
"comment": "Comment 1",
+ "history_entry_ids": [6, 9, 11],
},
)
self.assert_model_exists(
@@ -602,6 +673,7 @@ def test_merge_with_user_fields(self) -> None:
"meeting_id": 7,
"about_me": "I have a long beard",
"vote_weight": Decimal("1.234567"),
+ "history_entry_ids": [7, 12],
},
)
self.assert_model_exists(
@@ -610,6 +682,7 @@ def test_merge_with_user_fields(self) -> None:
"user_id": 2,
"meeting_id": 10,
"comment": "This is a comment",
+ "history_entry_ids": [14],
},
)
@@ -646,51 +719,117 @@ def test_merge_with_user_fields(self) -> None:
"timestamp": datetime.fromtimestamp(100000, ZoneInfo("UTC")),
"original_user_id": 1,
"user_id": 1,
- "entry_ids": [1, 2, 3],
+ "entry_ids": [1, 2, 3, 4, 5, 6, 7],
},
"history_entry/1": {
"original_model_id": "user/2",
"model_id": "user/2",
"position_id": 1,
- "entries": ["User created", "User added to meetings"],
+ "entries": [
+ "User created",
+ "User added to meetings",
+ "User added to groups 1, 2",
+ ],
},
"history_entry/2": {
+ "original_model_id": "meeting_user/12",
+ "model_id": "meeting_user/12",
+ "position_id": 1,
+ "changed_fields": {"group_ids": {"added": [1, 2]}},
+ },
+ "history_entry/3": {
"original_model_id": "user/3",
"model_id": "user/2",
"position_id": 1,
"entries": ["User created"],
},
- "history_entry/3": {
+ "history_entry/4": {
"original_model_id": "user/4",
"model_id": "user/2",
"position_id": 1,
- "entries": ["User created", "User added to meetings"],
+ "entries": [
+ "User created",
+ "User added to meetings",
+ "User added to groups 2, 4, 9",
+ ],
+ },
+ "history_entry/5": {
+ "original_model_id": "meeting_user/14",
+ "model_id": "meeting_user/12",
+ "position_id": 1,
+ "changed_fields": {"group_ids": {"added": [2]}},
+ },
+ "history_entry/6": {
+ "original_model_id": "meeting_user/44",
+ "model_id": "meeting_user/42",
+ "position_id": 1,
+ "changed_fields": {"group_ids": {"added": [4]}},
+ },
+ "history_entry/7": {
+ "original_model_id": "meeting_user/74",
+ "model_id": "meeting_user/106",
+ "position_id": 1,
+ "changed_fields": {"group_ids": {"added": [9]}},
},
"history_position/2": {
"timestamp": datetime.fromtimestamp(200000, ZoneInfo("UTC")),
"original_user_id": 4,
"user_id": 2,
- "entry_ids": [4, 5, 6, 7],
+ "entry_ids": [8, 9, 10, 11, 12, 13, 14, 15],
},
- "history_entry/4": {
+ "history_entry/8": {
"original_model_id": "user/2",
"model_id": "user/2",
"position_id": 2,
- "entries": ["User added to meetings"],
+ "entries": [
+ "User added to meetings",
+ "User added to group 5",
+ ],
},
- "history_entry/5": {
+ "history_entry/9": {
+ "original_model_id": "meeting_user/42",
+ "model_id": "meeting_user/42",
+ "position_id": 2,
+ "changed_fields": {"group_ids": {"added": [5]}},
+ },
+ "history_entry/10": {
"original_model_id": "user/3",
"model_id": "user/2",
"position_id": 2,
- "entries": ["User added to meetings"],
+ "entries": [
+ "User added to meetings",
+ "User added to groups 4, 5, 8",
+ ],
},
- "history_entry/6": {
+ "history_entry/11": {
+ "original_model_id": "meeting_user/43",
+ "model_id": "meeting_user/42",
+ "position_id": 2,
+ "changed_fields": {"group_ids": {"added": [4, 5]}},
+ },
+ "history_entry/12": {
+ "original_model_id": "meeting_user/73",
+ "model_id": "meeting_user/106",
+ "position_id": 2,
+ "changed_fields": {"group_ids": {"added": [8]}},
+ },
+ "history_entry/13": {
"original_model_id": "user/5",
"model_id": "user/2",
"position_id": 2,
- "entries": ["User created", "User added to meetings"],
+ "entries": [
+ "User created",
+ "User added to meetings",
+ "User added to group 10",
+ ],
},
- "history_entry/7": {
+ "history_entry/14": {
+ "original_model_id": "meeting_user/105",
+ "model_id": "meeting_user/107",
+ "position_id": 2,
+ "changed_fields": {"group_ids": {"added": [10]}},
+ },
+ "history_entry/15": {
"original_model_id": "user/6",
"model_id": "user/2",
"position_id": 2,
diff --git a/tests/system/action/user/test_set_present.py b/tests/system/action/user/test_set_present.py
index da18e3f5d0..32035968df 100644
--- a/tests/system/action/user/test_set_present.py
+++ b/tests/system/action/user/test_set_present.py
@@ -8,12 +8,10 @@ def setUp(self) -> None:
super().setUp()
self.create_meeting()
- def test_set_present_add_correct(self) -> None:
- self.set_models(
- {
- "user/111": {"username": "username_srtgb123"},
- }
- )
+ def base_test_set_present_add_correct(self, with_meeting_user: bool) -> None:
+ self.set_models({"user/111": {"username": "username_srtgb123"}})
+ if with_meeting_user:
+ self.set_user_groups(111, [3])
response = self.request(
"user.set_present", {"id": 111, "meeting_id": 1, "present": True}
)
@@ -23,10 +21,20 @@ def test_set_present_add_correct(self) -> None:
self.assert_history_information(
"user/111",
["Set present in meeting {}", "meeting/1"],
- {"is_present_in_meeting_ids": [1]},
)
+ if with_meeting_user:
+ self.assert_history_information(
+ "meeting_user/1",
+ changed_fields={"is_present": True},
+ )
- def test_set_present_add_second_correct(self) -> None:
+ def test_set_present_add_correct(self) -> None:
+ self.base_test_set_present_add_correct(False)
+
+ def test_set_present_add_correct_with_meeting_user(self) -> None:
+ self.base_test_set_present_add_correct(True)
+
+ def base_test_set_present_add_second_correct(self, with_meeting_user: bool) -> None:
self.set_models(
{
"meeting/1": {"present_user_ids": [111]},
@@ -34,6 +42,8 @@ def test_set_present_add_second_correct(self) -> None:
}
)
self.create_meeting(4)
+ if with_meeting_user:
+ self.set_user_groups(111, [3, 6])
response = self.request(
"user.set_present", {"id": 111, "meeting_id": 4, "present": True}
)
@@ -44,16 +54,35 @@ def test_set_present_add_second_correct(self) -> None:
self.assert_history_information(
"user/111",
["Set present in meeting {}", "meeting/4"],
- {"is_present_in_meeting_ids": [1, 4]},
)
+ if with_meeting_user:
+ self.assert_model_exists(
+ "meeting_user/1", {"user_id": 111, "meeting_id": 1}
+ )
+ self.assert_history_information("meeting_user/1")
+ self.assert_model_exists(
+ "meeting_user/2", {"user_id": 111, "meeting_id": 4}
+ )
+ self.assert_history_information(
+ "meeting_user/2",
+ changed_fields={"is_present": True},
+ )
- def test_set_present_del_correct(self) -> None:
+ def test_set_present_add_second_correct(self) -> None:
+ self.base_test_set_present_add_second_correct(False)
+
+ def test_set_present_add_second_correct_with_meeting_users(self) -> None:
+ self.base_test_set_present_add_second_correct(True)
+
+ def base_test_set_present_del_correct(self, with_meeting_user: bool) -> None:
self.set_models(
{
"meeting/1": {"present_user_ids": [111]},
"user/111": {"username": "username_srtgb123"},
}
)
+ if with_meeting_user:
+ self.set_user_groups(111, [3])
response = self.request(
"user.set_present", {"id": 111, "meeting_id": 1, "present": False}
)
@@ -63,8 +92,18 @@ def test_set_present_del_correct(self) -> None:
self.assert_history_information(
"user/111",
["Set not present in meeting {}", "meeting/1"],
- {"is_present_in_meeting_ids": []},
)
+ if with_meeting_user:
+ self.assert_history_information(
+ "meeting_user/1",
+ changed_fields={"is_present": False},
+ )
+
+ def test_set_present_del_correct(self) -> None:
+ self.base_test_set_present_del_correct(False)
+
+ def test_set_present_del_correct_with_meeting_user(self) -> None:
+ self.base_test_set_present_del_correct(True)
def test_set_present_null_action(self) -> None:
self.set_models(
diff --git a/tests/system/action/user/test_update.py b/tests/system/action/user/test_update.py
index 1299dd7480..4424976f79 100644
--- a/tests/system/action/user/test_update.py
+++ b/tests/system/action/user/test_update.py
@@ -289,11 +289,14 @@ def test_update_with_meeting_user_fields(self) -> None:
"meeting/1",
"Committee management changed",
],
- {"group_ids": [1]},
+ )
+ self.assert_history_information(
+ "meeting_user/224", changed_fields={"group_ids": {"added": [1]}}
)
self.assert_history_information(
"user/23", ["Vote delegated to {} in meeting {}", "user/22", "meeting/1"]
)
+ self.assert_history_information("meeting_user/223")
def test_update_set_and_reset_vote_forwarded(self) -> None:
self.create_meeting()
@@ -491,7 +494,10 @@ def test_committee_manager_without_committee_ids(self) -> None:
"Personal data changed",
"Committee management changed",
],
- {"group_ids": []},
+ )
+ self.assert_history_information(
+ "meeting_user/1111",
+ changed_fields={"group_ids": {"removed": [600]}},
)
def test_committee_manager_remove_committee_ids(self) -> None:
@@ -2682,8 +2688,12 @@ def test_update_history_add_group(self) -> None:
self.assert_history_information(
f"user/{user_id}",
["Participant added to group {} in meeting {}", "group/3", "meeting/1"],
- {"group_ids": [2, 3, 10, 11, 12]},
)
+ self.assert_history_information(
+ "meeting_user/1",
+ changed_fields={"group_ids": {"added": [3]}},
+ )
+ self.assert_history_information("meeting_user/2")
def test_update_history_add_group_to_default_group(self) -> None:
self.create_meeting()
@@ -2703,8 +2713,17 @@ def test_update_history_add_group_to_default_group(self) -> None:
self.assert_history_information(
f"user/{user_id}",
["Participant added to group {} in meeting {}", "group/2", "meeting/1"],
- {"group_ids": [2, 10, 11, 12]},
)
+ self.assert_history_information(
+ "meeting_user/1",
+ changed_fields={
+ "group_ids": {
+ "added": [2],
+ "removed": [1],
+ },
+ },
+ )
+ self.assert_history_information("meeting_user/2")
def test_update_history_add_multiple_groups(self) -> None:
self.create_meeting()
@@ -2724,8 +2743,17 @@ def test_update_history_add_multiple_groups(self) -> None:
self.assert_history_information(
f"user/{user_id}",
["Participant added to multiple groups in meeting {}", "meeting/1"],
- {"group_ids": [2, 3, 10, 11, 12]},
)
+ self.assert_history_information(
+ "meeting_user/1",
+ changed_fields={
+ "group_ids": {
+ "added": [2, 3],
+ "removed": [1],
+ },
+ },
+ )
+ self.assert_history_information("meeting_user/2")
def test_update_history_add_multiple_groups_with_default_group(self) -> None:
self.create_meeting()
@@ -2744,7 +2772,10 @@ def test_update_history_add_multiple_groups_with_default_group(self) -> None:
self.assert_history_information(
f"user/{user_id}",
["Participant added to group {} in meeting {}", "group/2", "meeting/1"],
- {"group_ids": [1, 2]},
+ )
+ self.assert_history_information(
+ "meeting_user/1",
+ changed_fields={"group_ids": {"added": [2]}},
)
def test_update_history_remove_group(self) -> None:
@@ -2769,7 +2800,10 @@ def test_update_history_remove_group(self) -> None:
self.assert_history_information(
f"user/{user_id}",
["Participant removed from meeting {}", "meeting/1"],
- {"group_ids": []},
+ )
+ self.assert_history_information(
+ "meeting_user/1",
+ changed_fields={"group_ids": {"removed": [1]}},
)
def test_update_fields_with_equal_value_no_history(self) -> None:
@@ -2866,7 +2900,11 @@ def test_update_participant_data_with_existing_meetings(self) -> None:
"group/4",
"meeting/4",
],
- {"group_ids": [1, 4]},
+ )
+ self.assert_history_information("meeting_user/1")
+ self.assert_history_information(
+ "meeting_user/2",
+ changed_fields={"group_ids": {"added": [4]}},
)
def test_update_participant_data_in_multiple_meetings_with_existing_meetings(
@@ -2916,7 +2954,113 @@ def test_update_participant_data_in_multiple_meetings_with_existing_meetings(
"group/7",
"meeting/7",
],
- {"group_ids": [1, 4, 7]},
+ )
+ self.assert_history_information("meeting_user/1")
+ self.assert_history_information(
+ "meeting_user/2",
+ changed_fields={"group_ids": {"added": [4]}},
+ )
+ self.assert_history_information(
+ "meeting_user/3",
+ changed_fields={"group_ids": {"added": [7]}},
+ )
+
+ def test_update_participant_data_in_multiple_meetings_log_is_present(
+ self,
+ ) -> None:
+ """
+ Checks that history information for the new meeting user contains
+ 'is_present' when user is present in meeting before the request.
+ """
+ bob_id = self.create_user("bob")
+ self.create_meeting(meeting_data={"present_user_ids": [bob_id]})
+ self.create_meeting(4, meeting_data={"present_user_ids": [bob_id]})
+ self.create_meeting(7)
+ self.set_user_groups(bob_id, [1])[0]
+ response = self.request_multi(
+ "user.update",
+ [
+ {
+ "id": bob_id,
+ "meeting_id": 4,
+ "group_ids": [4],
+ },
+ {
+ "id": bob_id,
+ "meeting_id": 7,
+ "group_ids": [7],
+ },
+ ],
+ )
+ self.assert_status_code(response, 200)
+ self.assert_history_information(
+ f"user/{bob_id}",
+ [
+ "Participant added to meeting {}.",
+ "meeting/4",
+ "Participant added to group {} in meeting {}.",
+ "group/4",
+ "meeting/4",
+ "Participant added to meeting {}.",
+ "meeting/7",
+ "Participant added to group {} in meeting {}.",
+ "group/7",
+ "meeting/7",
+ ],
+ )
+ self.assert_history_information("meeting_user/1")
+ self.assert_history_information(
+ "meeting_user/2",
+ changed_fields={"group_ids": {"added": [4]}, "is_present": True},
+ )
+ self.assert_history_information(
+ "meeting_user/3",
+ changed_fields={"group_ids": {"added": [7]}},
+ )
+
+ def test_update_remove_participant_from_multiple_meetings_log_is_present(
+ self,
+ ) -> None:
+ bob_id = self.create_user("bob")
+ self.create_meeting(meeting_data={"present_user_ids": [bob_id]})
+ self.create_meeting(4, meeting_data={"present_user_ids": [bob_id]})
+ self.create_meeting(7)
+ self.set_user_groups(bob_id, [1, 4, 7])
+ response = self.request_multi(
+ "user.update",
+ [
+ {
+ "id": bob_id,
+ "meeting_id": 4,
+ "group_ids": [],
+ },
+ {
+ "id": bob_id,
+ "meeting_id": 7,
+ "group_ids": [],
+ },
+ ],
+ )
+ self.assert_status_code(response, 200)
+ self.assert_history_information(
+ f"user/{bob_id}",
+ [
+ "Set not present in meeting {}",
+ "meeting/4",
+ "Participant removed from meeting {}",
+ "meeting/4",
+ "Participant removed from meeting {}",
+ "meeting/7",
+ ],
+ )
+ self.assert_history_information("meeting_user/1")
+ self.assert_history_information(
+ "meeting_user/2",
+ changed_fields={"group_ids": {"removed": [4]}, "is_present": False},
+ )
+ self.assert_history_information(
+ "meeting_user/3",
+ changed_fields={"group_ids": {"removed": [7]}},
)
def test_update_saml_id__can_change_own_password_error(self) -> None:
From 78e4741880df4f9bd9670af4750cc9dbd544c997 Mon Sep 17 00:00:00 2001
From: Viktoriia Krasnovyd <114735598+vkrasnovyd@users.noreply.github.com>
Date: Fri, 17 Jul 2026 11:19:24 +0200
Subject: [PATCH 21/23] [vote] Rename changed_fields (#3676)
---
meta | 2 +-
openslides_backend/action/action.py | 6 ++-
.../actions/meeting_user/base_delete.py | 4 +-
.../actions/meeting_user/history_mixin.py | 42 ++++++++++---------
.../action/actions/user/set_present.py | 2 +-
openslides_backend/models/models.py | 2 +-
openslides_backend/shared/history_events.py | 30 +++++++------
openslides_backend/shared/typing.py | 2 +-
tests/database/reader/system/test_filter.py | 2 +-
tests/system/action/base.py | 10 +++--
tests/system/action/user/test_create.py | 2 +-
.../action/user/test_delegation_history.py | 16 +++----
.../system/action/user/test_merge_together.py | 32 +++++++-------
tests/system/action/user/test_set_present.py | 6 +--
tests/system/action/user/test_update.py | 28 ++++++-------
15 files changed, 99 insertions(+), 87 deletions(-)
diff --git a/meta b/meta
index 5dcd6aeb03..c6e391473e 160000
--- a/meta
+++ b/meta
@@ -1 +1 @@
-Subproject commit 5dcd6aeb03b83cc2620fae92222c9197c5b5da02
+Subproject commit c6e391473ea75459b61c527731776c6f5fd2937a
diff --git a/openslides_backend/action/action.py b/openslides_backend/action/action.py
index f2d3e61689..53f9de661e 100644
--- a/openslides_backend/action/action.py
+++ b/openslides_backend/action/action.py
@@ -781,8 +781,10 @@ def merge_history_informations(
if fqid in a:
if entries := information.get("entries"):
a[fqid].setdefault("entries", list()).extend(entries)
- if changed_fields := information.get("changed_fields"):
- a[fqid].setdefault("changed_fields", dict()).update(changed_fields)
+ if structured_information := information.get("structured_information"):
+ a[fqid].setdefault("structured_information", dict()).update(
+ structured_information
+ )
else:
a[fqid] = information
return a
diff --git a/openslides_backend/action/actions/meeting_user/base_delete.py b/openslides_backend/action/actions/meeting_user/base_delete.py
index af6465b1df..9f39d9677e 100644
--- a/openslides_backend/action/actions/meeting_user/base_delete.py
+++ b/openslides_backend/action/actions/meeting_user/base_delete.py
@@ -45,5 +45,7 @@ def get_history_information(self) -> HistoryInformation | None:
):
information[
fqid_from_collection_and_id("meeting_user", meeting_user["id"])
- ] = build_history_information_data(changed_fields={"is_present": False})
+ ] = build_history_information_data(
+ structured_information={"is_present": False}
+ )
return information
diff --git a/openslides_backend/action/actions/meeting_user/history_mixin.py b/openslides_backend/action/actions/meeting_user/history_mixin.py
index 49fe694acc..260599b13b 100644
--- a/openslides_backend/action/actions/meeting_user/history_mixin.py
+++ b/openslides_backend/action/actions/meeting_user/history_mixin.py
@@ -15,7 +15,7 @@
class ActionHistoryInformationData(TypedDict):
entries: NotRequired[list[tuple[str, ...]]]
- changed_fields: NotRequired[dict[Field, Any]]
+ structured_information: NotRequired[dict[Field, Any]]
ActionHistoryInformation = dict[FullQualifiedId, ActionHistoryInformationData]
@@ -109,7 +109,7 @@ def get_history_information(self) -> HistoryInformation | None:
return {
fqid: build_history_information_data(
[string for entry in history.get("entries", []) for string in entry],
- history.get("changed_fields", {}),
+ history.get("structured_information", {}),
)
for fqid, history in information.items()
}
@@ -121,7 +121,7 @@ def add_updated_meeting_user_history_information(
information: ActionHistoryInformation,
) -> None:
user_entries: list[tuple[str, ...]] = []
- meeting_user_changed_fields: dict[str, dict[str, list[int]]] = {}
+ meeting_user_structured_information: dict[str, dict[str, list[int]]] = {}
user_id = db_instance["user_id"]
meeting_id = db_instance["meeting_id"]
@@ -143,7 +143,7 @@ def add_updated_meeting_user_history_information(
self.handle_group_updates(
user_entries,
- meeting_user_changed_fields,
+ meeting_user_structured_information,
instance,
db_instance,
)
@@ -155,10 +155,10 @@ def add_updated_meeting_user_history_information(
user_entries,
for_user_id=user_id,
)
- if meeting_user_changed_fields:
+ if meeting_user_structured_information:
information[
fqid_from_collection_and_id("meeting_user", db_instance["id"])
- ] = {"changed_fields": meeting_user_changed_fields}
+ ] = {"structured_information": meeting_user_structured_information}
def add_created_meeting_user_history_information(
self,
@@ -205,7 +205,7 @@ def add_created_meeting_user_history_information(
for_user_id=db_instance["user_id"],
)
if "group_ids" in instance:
- changed_fields: dict[str, dict[str, list[int]] | bool] = {
+ structured_information: dict[str, dict[str, list[int]] | bool] = {
"group_ids": {"added": instance["group_ids"]}
}
is_present_in_meeting_ids = self.datastore.get(
@@ -214,9 +214,9 @@ def add_created_meeting_user_history_information(
lock_result=False,
).get("is_present_in_meeting_ids", [])
if db_instance["meeting_id"] in is_present_in_meeting_ids:
- changed_fields["is_present"] = True
+ structured_information["is_present"] = True
information[fqid_from_collection_and_id("meeting_user", instance["id"])] = {
- "changed_fields": changed_fields
+ "structured_information": structured_information
}
def add_entries_to_history_information(
@@ -225,7 +225,7 @@ def add_entries_to_history_information(
entries: list[tuple[str, ...]] = [],
for_user_id: int | None = None,
for_meeting_user_id: int | None = None,
- changed_fields: dict[Field, Any] | None = None,
+ structured_information: dict[Field, Any] | None = None,
) -> None:
if not for_user_id:
if not for_meeting_user_id:
@@ -241,15 +241,15 @@ def add_entries_to_history_information(
if fqid not in information:
if entries:
information[fqid] = {"entries": entries}
- if changed_fields:
- information[fqid]["changed_fields"] = changed_fields
+ if structured_information:
+ information[fqid]["structured_information"] = structured_information
else:
for entry in entries:
if entry not in information[fqid]["entries"]:
information[fqid]["entries"].append(entry)
- if changed_fields:
- information[fqid].setdefault("changed_fields", dict()).update(
- changed_fields
+ if structured_information:
+ information[fqid].setdefault("structured_information", dict()).update(
+ structured_information
)
def compose_history_string(
@@ -281,7 +281,7 @@ def compose_history_string(
def handle_group_updates(
self,
entries: list[tuple[str, ...]],
- meeting_user_changed_fields: dict[str, dict[str, list[int]]],
+ meeting_user_structured_information: dict[str, dict[str, list[int]]],
instance: dict[str, Any],
db_instance: dict[str, Any],
) -> None:
@@ -294,11 +294,13 @@ def handle_group_updates(
# Calculate history information for meeting_user
if added:
- meeting_user_changed_fields["group_ids"] = {"added": list(added)}
+ meeting_user_structured_information["group_ids"] = {
+ "added": list(added)
+ }
if removed:
- meeting_user_changed_fields.setdefault("group_ids", dict()).update(
- {"removed": list(removed)}
- )
+ meeting_user_structured_information.setdefault(
+ "group_ids", dict()
+ ).update({"removed": list(removed)})
# remove default groups
meeting = self.datastore.get(
diff --git a/openslides_backend/action/actions/user/set_present.py b/openslides_backend/action/actions/user/set_present.py
index 0e3e044d16..0c0748878e 100644
--- a/openslides_backend/action/actions/user/set_present.py
+++ b/openslides_backend/action/actions/user/set_present.py
@@ -131,6 +131,6 @@ def get_history_information(self) -> HistoryInformation | None:
information[
fqid_from_collection_and_id("meeting_user", meeting_user["id"])
] = build_history_information_data(
- changed_fields={"is_present": present}
+ structured_information={"is_present": present}
)
return information
diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py
index 57aa2322e5..bf17bf33df 100644
--- a/openslides_backend/models/models.py
+++ b/openslides_backend/models/models.py
@@ -529,7 +529,7 @@ class HistoryEntry(Model):
id = fields.IntegerField(required=True, constant=True)
entries = fields.TextArrayField()
- changed_fields = fields.JSONField()
+ structured_information = fields.JSONField()
original_model_id = fields.CharField(constant=True)
model_id = fields.GenericRelationField(
to={
diff --git a/openslides_backend/shared/history_events.py b/openslides_backend/shared/history_events.py
index 796b35e9b3..8f88a6404e 100644
--- a/openslides_backend/shared/history_events.py
+++ b/openslides_backend/shared/history_events.py
@@ -13,13 +13,13 @@
def build_history_information_data(
entries: list[str] | None = None,
- changed_fields: dict[str, Any] | None = None,
+ structured_information: dict[str, Any] | None = None,
) -> HistoryInformationData:
data: HistoryInformationData = {}
if entries is not None:
data["entries"] = entries
- if changed_fields is not None:
- data["changed_fields"] = changed_fields
+ if structured_information is not None:
+ data["structured_information"] = structured_information
return data
@@ -27,17 +27,19 @@ def update_history_information(
information: HistoryInformation,
fqid: FullQualifiedId,
entries: list[str] | None = None,
- changed_fields: dict[str, Any] | None = None,
+ structured_information: dict[str, Any] | None = None,
) -> None:
"""Updates history information for fqid"""
if fqid not in information:
- information[fqid] = build_history_information_data(entries, changed_fields)
+ information[fqid] = build_history_information_data(
+ entries, structured_information
+ )
else:
if entries:
information[fqid].setdefault("entries", list()).extend(entries)
- if changed_fields:
- information[fqid].setdefault("changed_fields", dict()).update(
- changed_fields
+ if structured_information:
+ information[fqid].setdefault("structured_information", dict()).update(
+ structured_information
)
@@ -45,13 +47,13 @@ def update_history_information_multi(
information: HistoryInformation,
fqids: list[FullQualifiedId],
entries: list[str] | None = None,
- changed_fields: dict[str, Any] | None = None,
+ structured_information: dict[str, Any] | None = None,
) -> None:
"""
Adds given HistoryInformation to the given information for every fqid in fqids.
"""
for fqid in fqids:
- update_history_information(information, fqid, entries, changed_fields)
+ update_history_information(information, fqid, entries, structured_information)
def calculate_history_event_payloads(
@@ -68,7 +70,7 @@ def calculate_history_event_payloads(
model_fqid_to_entry_id[fqid],
fqid,
data.get("entries") or None,
- data.get("changed_fields"),
+ data.get("structured_information"),
)
for fqid, data in information.items()
]
@@ -78,14 +80,16 @@ def calculate_history_event_payloads(
{
"id": id_,
"entries": entries,
- "changed_fields": Jsonb(changed_fields) if changed_fields else None,
+ "structured_information": (
+ Jsonb(structured_information) if structured_information else None
+ ),
"position_id": position_id,
"original_model_id": fqid,
"model_id": (fqid if fqid in existing_fqids else None),
"meeting_id": model_fqid_to_meeting_id.get(fqid, None),
},
)
- for id_, fqid, entries, changed_fields in transformed_information
+ for id_, fqid, entries, structured_information in transformed_information
]
create_events.append(
(
diff --git a/openslides_backend/shared/typing.py b/openslides_backend/shared/typing.py
index e0a2d531f6..e8d2b03c3f 100644
--- a/openslides_backend/shared/typing.py
+++ b/openslides_backend/shared/typing.py
@@ -11,7 +11,7 @@
class HistoryInformationData(TypedDict):
entries: NotRequired[list[str]]
- changed_fields: NotRequired[dict[Field, Any]]
+ structured_information: NotRequired[dict[Field, Any]]
HistoryInformation = dict[FullQualifiedId, HistoryInformationData]
diff --git a/tests/database/reader/system/test_filter.py b/tests/database/reader/system/test_filter.py
index e3704b671b..624e39b42f 100644
--- a/tests/database/reader/system/test_filter.py
+++ b/tests/database/reader/system/test_filter.py
@@ -176,7 +176,7 @@ def test_types_str_list(db_connection: Connection) -> None:
1: {
"id": 1,
"entries": ["User added to meetings"],
- "changed_fields": None,
+ "structured_information": None,
"meeting_id": 1,
"model_id": "user/1",
"model_id_assignment_id": None,
diff --git a/tests/system/action/base.py b/tests/system/action/base.py
index e7cb6cc4bb..64efcd363d 100644
--- a/tests/system/action/base.py
+++ b/tests/system/action/base.py
@@ -310,7 +310,7 @@ def get_last_history_information(
if entry_id:
return self.datastore.get(
fqid_from_collection_and_id("history_entry", entry_id),
- ["entries", "changed_fields"],
+ ["entries", "structured_information"],
lock_result=False,
)
else:
@@ -320,13 +320,13 @@ def assert_history_information(
self,
fqid: FullQualifiedId,
entries: list[str] | None = None,
- changed_fields: dict[str, Any] | None = None,
+ structured_information: dict[str, Any] | None = None,
) -> None:
"""
Asserts that the last history information for the given model is the given information.
"""
last_information = self.get_last_history_information(fqid)
- if entries is None and changed_fields is None:
+ if entries is None and structured_information is None:
assert (
not last_information
), f"Expected no history information to be generated for {fqid}. Got:\n{last_information}"
@@ -335,7 +335,9 @@ def assert_history_information(
last_information
), f"No history information was be generated for {fqid}."
self.assertEqual(last_information.get("entries"), entries)
- self.assertEqual(last_information.get("changed_fields"), changed_fields)
+ self.assertEqual(
+ last_information.get("structured_information"), structured_information
+ )
def assert_history_information_contains(
self, fqid: FullQualifiedId, entry: str
diff --git a/tests/system/action/user/test_create.py b/tests/system/action/user/test_create.py
index d4af5c602c..b1379a791e 100644
--- a/tests/system/action/user/test_create.py
+++ b/tests/system/action/user/test_create.py
@@ -136,7 +136,7 @@ def test_create_some_more_fields(self) -> None:
)
self.assert_history_information(
"meeting_user/1",
- changed_fields={"group_ids": {"added": [114]}},
+ structured_information={"group_ids": {"added": [114]}},
)
def test_create_comment(self) -> None:
diff --git a/tests/system/action/user/test_delegation_history.py b/tests/system/action/user/test_delegation_history.py
index a5f3531475..d9d0d822eb 100644
--- a/tests/system/action/user/test_delegation_history.py
+++ b/tests/system/action/user/test_delegation_history.py
@@ -94,14 +94,14 @@ def assert_alice_redelegated_to(self, who_id: int, prepend: list[str] = []) -> N
],
)
- def assert_meeting_user_changed_fields(self, user_id: int) -> None:
+ def assert_meeting_user_structured_information(self, user_id: int) -> None:
self.assert_model_exists(
f"meeting_user/{self.next_meeting_user_id}",
{"user_id": user_id, "meeting_id": 1, "group_ids": [3]},
)
self.assert_history_information(
f"meeting_user/{self.next_meeting_user_id}",
- changed_fields={"group_ids": {"added": [3]}},
+ structured_information={"group_ids": {"added": [3]}},
)
def test_update_delegate_vote(self) -> None:
@@ -128,7 +128,7 @@ def test_create_delegate_vote(self) -> None:
"meeting/1",
],
)
- self.assert_meeting_user_changed_fields(self.next_user_id)
+ self.assert_meeting_user_structured_information(self.next_user_id)
def test_create_receive_delegated_vote(self) -> None:
self.make_request(
@@ -146,7 +146,7 @@ def test_create_receive_delegated_vote(self) -> None:
"meeting/1",
],
)
- self.assert_meeting_user_changed_fields(self.next_user_id)
+ self.assert_meeting_user_structured_information(self.next_user_id)
def test_update_re_delegate_vote(self) -> None:
self.setup_delegation()
@@ -176,7 +176,7 @@ def test_create_re_delegate_vote_reverse(self) -> None:
"meeting/1",
],
)
- self.assert_meeting_user_changed_fields(self.next_user_id)
+ self.assert_meeting_user_structured_information(self.next_user_id)
def test_update_re_delegate_received_votes(self) -> None:
self.setup_delegation()
@@ -387,7 +387,7 @@ def test_create_multiple_from_ids(self) -> None:
"meeting/1",
],
)
- self.assert_meeting_user_changed_fields(self.next_user_id + 2)
+ self.assert_meeting_user_structured_information(self.next_user_id + 2)
for id_ in [self.alice_id, self.bob_id, self.colin_id, eric_id, fredric_id]:
self.assert_history_information(
f"user/{id_}",
@@ -421,7 +421,7 @@ def test_update_create_meeting_user_receiving_delegation(self) -> None:
"meeting/1",
],
)
- self.assert_meeting_user_changed_fields(debra_id)
+ self.assert_meeting_user_structured_information(debra_id)
def test_update_create_meeting_user_with_delegation(self) -> None:
debra_id = self.create_user("debra")
@@ -449,4 +449,4 @@ def test_update_create_meeting_user_with_delegation(self) -> None:
"meeting/1",
],
)
- self.assert_meeting_user_changed_fields(debra_id)
+ self.assert_meeting_user_structured_information(debra_id)
diff --git a/tests/system/action/user/test_merge_together.py b/tests/system/action/user/test_merge_together.py
index b8e6159507..8e1e269564 100644
--- a/tests/system/action/user/test_merge_together.py
+++ b/tests/system/action/user/test_merge_together.py
@@ -499,7 +499,7 @@ def setup_complex_user_fields(self) -> None:
"original_model_id": "meeting_user/12",
"model_id": "meeting_user/12",
"position_id": 1,
- "changed_fields": Jsonb({"group_ids": {"added": [1, 2]}}),
+ "structured_information": Jsonb({"group_ids": {"added": [1, 2]}}),
},
"history_entry/3": {
"original_model_id": "user/3",
@@ -521,19 +521,19 @@ def setup_complex_user_fields(self) -> None:
"original_model_id": "meeting_user/14",
"model_id": "meeting_user/14",
"position_id": 1,
- "changed_fields": Jsonb({"group_ids": {"added": [2]}}),
+ "structured_information": Jsonb({"group_ids": {"added": [2]}}),
},
"history_entry/6": {
"original_model_id": "meeting_user/44",
"model_id": "meeting_user/44",
"position_id": 1,
- "changed_fields": Jsonb({"group_ids": {"added": [4]}}),
+ "structured_information": Jsonb({"group_ids": {"added": [4]}}),
},
"history_entry/7": {
"original_model_id": "meeting_user/74",
"model_id": "meeting_user/74",
"position_id": 1,
- "changed_fields": Jsonb({"group_ids": {"added": [9]}}),
+ "structured_information": Jsonb({"group_ids": {"added": [9]}}),
},
"history_position/2": {
"timestamp": datetime.fromtimestamp(200000, ZoneInfo("UTC")),
@@ -553,7 +553,7 @@ def setup_complex_user_fields(self) -> None:
"original_model_id": "meeting_user/42",
"model_id": "meeting_user/42",
"position_id": 2,
- "changed_fields": Jsonb({"group_ids": {"added": [5]}}),
+ "structured_information": Jsonb({"group_ids": {"added": [5]}}),
},
"history_entry/10": {
"original_model_id": "user/3",
@@ -568,13 +568,13 @@ def setup_complex_user_fields(self) -> None:
"original_model_id": "meeting_user/43",
"model_id": "meeting_user/43",
"position_id": 2,
- "changed_fields": Jsonb({"group_ids": {"added": [4, 5]}}),
+ "structured_information": Jsonb({"group_ids": {"added": [4, 5]}}),
},
"history_entry/12": {
"original_model_id": "meeting_user/73",
"model_id": "meeting_user/73",
"position_id": 2,
- "changed_fields": Jsonb({"group_ids": {"added": [8]}}),
+ "structured_information": Jsonb({"group_ids": {"added": [8]}}),
},
"history_entry/13": {
"original_model_id": "user/5",
@@ -590,7 +590,7 @@ def setup_complex_user_fields(self) -> None:
"original_model_id": "meeting_user/105",
"model_id": "meeting_user/105",
"position_id": 2,
- "changed_fields": Jsonb({"group_ids": {"added": [10]}}),
+ "structured_information": Jsonb({"group_ids": {"added": [10]}}),
},
"history_entry/15": {
"original_model_id": "user/6",
@@ -735,7 +735,7 @@ def test_merge_with_user_fields(self) -> None:
"original_model_id": "meeting_user/12",
"model_id": "meeting_user/12",
"position_id": 1,
- "changed_fields": {"group_ids": {"added": [1, 2]}},
+ "structured_information": {"group_ids": {"added": [1, 2]}},
},
"history_entry/3": {
"original_model_id": "user/3",
@@ -757,19 +757,19 @@ def test_merge_with_user_fields(self) -> None:
"original_model_id": "meeting_user/14",
"model_id": "meeting_user/12",
"position_id": 1,
- "changed_fields": {"group_ids": {"added": [2]}},
+ "structured_information": {"group_ids": {"added": [2]}},
},
"history_entry/6": {
"original_model_id": "meeting_user/44",
"model_id": "meeting_user/42",
"position_id": 1,
- "changed_fields": {"group_ids": {"added": [4]}},
+ "structured_information": {"group_ids": {"added": [4]}},
},
"history_entry/7": {
"original_model_id": "meeting_user/74",
"model_id": "meeting_user/106",
"position_id": 1,
- "changed_fields": {"group_ids": {"added": [9]}},
+ "structured_information": {"group_ids": {"added": [9]}},
},
"history_position/2": {
"timestamp": datetime.fromtimestamp(200000, ZoneInfo("UTC")),
@@ -790,7 +790,7 @@ def test_merge_with_user_fields(self) -> None:
"original_model_id": "meeting_user/42",
"model_id": "meeting_user/42",
"position_id": 2,
- "changed_fields": {"group_ids": {"added": [5]}},
+ "structured_information": {"group_ids": {"added": [5]}},
},
"history_entry/10": {
"original_model_id": "user/3",
@@ -805,13 +805,13 @@ def test_merge_with_user_fields(self) -> None:
"original_model_id": "meeting_user/43",
"model_id": "meeting_user/42",
"position_id": 2,
- "changed_fields": {"group_ids": {"added": [4, 5]}},
+ "structured_information": {"group_ids": {"added": [4, 5]}},
},
"history_entry/12": {
"original_model_id": "meeting_user/73",
"model_id": "meeting_user/106",
"position_id": 2,
- "changed_fields": {"group_ids": {"added": [8]}},
+ "structured_information": {"group_ids": {"added": [8]}},
},
"history_entry/13": {
"original_model_id": "user/5",
@@ -827,7 +827,7 @@ def test_merge_with_user_fields(self) -> None:
"original_model_id": "meeting_user/105",
"model_id": "meeting_user/107",
"position_id": 2,
- "changed_fields": {"group_ids": {"added": [10]}},
+ "structured_information": {"group_ids": {"added": [10]}},
},
"history_entry/15": {
"original_model_id": "user/6",
diff --git a/tests/system/action/user/test_set_present.py b/tests/system/action/user/test_set_present.py
index 32035968df..024928200d 100644
--- a/tests/system/action/user/test_set_present.py
+++ b/tests/system/action/user/test_set_present.py
@@ -25,7 +25,7 @@ def base_test_set_present_add_correct(self, with_meeting_user: bool) -> None:
if with_meeting_user:
self.assert_history_information(
"meeting_user/1",
- changed_fields={"is_present": True},
+ structured_information={"is_present": True},
)
def test_set_present_add_correct(self) -> None:
@@ -65,7 +65,7 @@ def base_test_set_present_add_second_correct(self, with_meeting_user: bool) -> N
)
self.assert_history_information(
"meeting_user/2",
- changed_fields={"is_present": True},
+ structured_information={"is_present": True},
)
def test_set_present_add_second_correct(self) -> None:
@@ -96,7 +96,7 @@ def base_test_set_present_del_correct(self, with_meeting_user: bool) -> None:
if with_meeting_user:
self.assert_history_information(
"meeting_user/1",
- changed_fields={"is_present": False},
+ structured_information={"is_present": False},
)
def test_set_present_del_correct(self) -> None:
diff --git a/tests/system/action/user/test_update.py b/tests/system/action/user/test_update.py
index 4424976f79..8644285743 100644
--- a/tests/system/action/user/test_update.py
+++ b/tests/system/action/user/test_update.py
@@ -291,7 +291,7 @@ def test_update_with_meeting_user_fields(self) -> None:
],
)
self.assert_history_information(
- "meeting_user/224", changed_fields={"group_ids": {"added": [1]}}
+ "meeting_user/224", structured_information={"group_ids": {"added": [1]}}
)
self.assert_history_information(
"user/23", ["Vote delegated to {} in meeting {}", "user/22", "meeting/1"]
@@ -497,7 +497,7 @@ def test_committee_manager_without_committee_ids(self) -> None:
)
self.assert_history_information(
"meeting_user/1111",
- changed_fields={"group_ids": {"removed": [600]}},
+ structured_information={"group_ids": {"removed": [600]}},
)
def test_committee_manager_remove_committee_ids(self) -> None:
@@ -2691,7 +2691,7 @@ def test_update_history_add_group(self) -> None:
)
self.assert_history_information(
"meeting_user/1",
- changed_fields={"group_ids": {"added": [3]}},
+ structured_information={"group_ids": {"added": [3]}},
)
self.assert_history_information("meeting_user/2")
@@ -2716,7 +2716,7 @@ def test_update_history_add_group_to_default_group(self) -> None:
)
self.assert_history_information(
"meeting_user/1",
- changed_fields={
+ structured_information={
"group_ids": {
"added": [2],
"removed": [1],
@@ -2746,7 +2746,7 @@ def test_update_history_add_multiple_groups(self) -> None:
)
self.assert_history_information(
"meeting_user/1",
- changed_fields={
+ structured_information={
"group_ids": {
"added": [2, 3],
"removed": [1],
@@ -2775,7 +2775,7 @@ def test_update_history_add_multiple_groups_with_default_group(self) -> None:
)
self.assert_history_information(
"meeting_user/1",
- changed_fields={"group_ids": {"added": [2]}},
+ structured_information={"group_ids": {"added": [2]}},
)
def test_update_history_remove_group(self) -> None:
@@ -2803,7 +2803,7 @@ def test_update_history_remove_group(self) -> None:
)
self.assert_history_information(
"meeting_user/1",
- changed_fields={"group_ids": {"removed": [1]}},
+ structured_information={"group_ids": {"removed": [1]}},
)
def test_update_fields_with_equal_value_no_history(self) -> None:
@@ -2904,7 +2904,7 @@ def test_update_participant_data_with_existing_meetings(self) -> None:
self.assert_history_information("meeting_user/1")
self.assert_history_information(
"meeting_user/2",
- changed_fields={"group_ids": {"added": [4]}},
+ structured_information={"group_ids": {"added": [4]}},
)
def test_update_participant_data_in_multiple_meetings_with_existing_meetings(
@@ -2958,11 +2958,11 @@ def test_update_participant_data_in_multiple_meetings_with_existing_meetings(
self.assert_history_information("meeting_user/1")
self.assert_history_information(
"meeting_user/2",
- changed_fields={"group_ids": {"added": [4]}},
+ structured_information={"group_ids": {"added": [4]}},
)
self.assert_history_information(
"meeting_user/3",
- changed_fields={"group_ids": {"added": [7]}},
+ structured_information={"group_ids": {"added": [7]}},
)
def test_update_participant_data_in_multiple_meetings_log_is_present(
@@ -3011,11 +3011,11 @@ def test_update_participant_data_in_multiple_meetings_log_is_present(
self.assert_history_information("meeting_user/1")
self.assert_history_information(
"meeting_user/2",
- changed_fields={"group_ids": {"added": [4]}, "is_present": True},
+ structured_information={"group_ids": {"added": [4]}, "is_present": True},
)
self.assert_history_information(
"meeting_user/3",
- changed_fields={"group_ids": {"added": [7]}},
+ structured_information={"group_ids": {"added": [7]}},
)
def test_update_remove_participant_from_multiple_meetings_log_is_present(
@@ -3056,11 +3056,11 @@ def test_update_remove_participant_from_multiple_meetings_log_is_present(
self.assert_history_information("meeting_user/1")
self.assert_history_information(
"meeting_user/2",
- changed_fields={"group_ids": {"removed": [4]}, "is_present": False},
+ structured_information={"group_ids": {"removed": [4]}, "is_present": False},
)
self.assert_history_information(
"meeting_user/3",
- changed_fields={"group_ids": {"removed": [7]}},
+ structured_information={"group_ids": {"removed": [7]}},
)
def test_update_saml_id__can_change_own_password_error(self) -> None:
From f389145051b4cfa9758fab375ec045abae5c5155 Mon Sep 17 00:00:00 2001
From: Viktoriia Krasnovyd <114735598+vkrasnovyd@users.noreply.github.com>
Date: Fri, 17 Jul 2026 15:18:27 +0200
Subject: [PATCH 22/23] [vote] Move user information from poll_ballot to
separate collection (#3667)
---
data/example-data.json | 21 ++-
docs/Actions-Overview.md | 1 +
docs/actions/meeting_user.update.md | 1 -
docs/actions/poll_ballot_user.delete.md | 9 +
meta | 2 +-
openslides_backend/action/action.py | 10 +-
openslides_backend/action/actions/__init__.py | 1 +
.../action/actions/meeting_user/update.py | 5 +-
.../actions/poll_ballot_user/__init__.py | 1 +
.../action/actions/poll_ballot_user/delete.py | 15 ++
.../action/actions/user/merge_mixins.py | 10 +-
openslides_backend/action/generics/delete.py | 3 -
openslides_backend/models/checker.py | 1 +
openslides_backend/models/models.py | 47 ++---
openslides_backend/shared/export_helper.py | 2 +-
tests/system/action/meeting/test_clone.py | 9 +
tests/system/action/meeting/test_delete.py | 1 +
tests/system/action/meeting/test_import.py | 25 +--
.../system/action/meeting_user/test_update.py | 25 ++-
.../system/action/user/test_merge_together.py | 165 ++++++++++++++----
tests/system/presenter/test_check_database.py | 4 +
.../presenter/test_check_database_all.py | 4 +
tests/system/presenter/test_export_meeting.py | 17 +-
23 files changed, 269 insertions(+), 110 deletions(-)
create mode 100644 docs/actions/poll_ballot_user.delete.md
create mode 100644 openslides_backend/action/actions/poll_ballot_user/__init__.py
create mode 100644 openslides_backend/action/actions/poll_ballot_user/delete.py
diff --git a/data/example-data.json b/data/example-data.json
index 6b037ed50e..663fec2c0b 100644
--- a/data/example-data.json
+++ b/data/example-data.json
@@ -157,10 +157,9 @@
"assignment_candidate_ids": [1],
"structure_level_ids": [1],
"group_ids": [2],
- "poll_voted_ids": [3],
"poll_option_ids": [1, 8],
- "acting_ballot_ids": [9],
- "represented_ballot_ids": [9]
+ "acting_ballot_ids": [1],
+ "represented_ballot_ids": [1]
},
"2": {
"id": 2,
@@ -1952,7 +1951,7 @@
"allow_vote_split": false,
"content_object_id": "motion/1",
"ballot_ids": [9],
- "voted_ids": [1],
+ "ballot_user_ids": [1],
"meeting_id": 1
},
"4": {
@@ -2178,12 +2177,20 @@
"id": 9,
"value": "1",
"weight": "1.000000",
+ "split": false,
+ "poll_id": 3,
+ "poll_ballot_user_id": 1
+ }
+ },
+ "poll_ballot_user": {
+ "1": {
+ "id": 1,
"acting_meeting_user_id": 1,
"represented_meeting_user_id": 1,
- "split": false,
- "poll_id": 3
+ "poll_id": 3,
+ "poll_ballot_id": 9
}
- },
+ },
"assignment": {
"1": {
"id": 1,
diff --git a/docs/Actions-Overview.md b/docs/Actions-Overview.md
index 09e272afa2..f37c9221cd 100644
--- a/docs/Actions-Overview.md
+++ b/docs/Actions-Overview.md
@@ -182,6 +182,7 @@ A more general format description see in [Action-Service](https://github.com/Ope
- [poll.delete](actions/poll.delete.md)
- [poll_ballot.delete](actions/poll_ballot.delete.md)
+- [poll_ballot_user.delete](actions/poll_ballot_user.delete.md)
- [poll_config_approval.delete](actions/poll_config_approval.delete.md)
- [poll_config_rating_approval.delete](actions/poll_config_rating_approval.delete.md)
- [poll_config_rating_score.delete](actions/poll_config_rating_score.delete.md)
diff --git a/docs/actions/meeting_user.update.md b/docs/actions/meeting_user.update.md
index aeb77b500d..d632eac4fe 100644
--- a/docs/actions/meeting_user.update.md
+++ b/docs/actions/meeting_user.update.md
@@ -6,7 +6,6 @@
// Optional
poll_option_ids: Id[];
- poll_voted_ids: Id[];
acting_ballot_ids: Id[];
represented_ballot_ids: Id[];
diff --git a/docs/actions/poll_ballot_user.delete.md b/docs/actions/poll_ballot_user.delete.md
new file mode 100644
index 0000000000..f73c6b9778
--- /dev/null
+++ b/docs/actions/poll_ballot_user.delete.md
@@ -0,0 +1,9 @@
+## Payload
+
+```js
+{ id: Id; }
+```
+
+## Internal action
+
+Deletes a poll_ballot_user.
diff --git a/meta b/meta
index c6e391473e..6d54f6540a 160000
--- a/meta
+++ b/meta
@@ -1 +1 @@
-Subproject commit c6e391473ea75459b61c527731776c6f5fd2937a
+Subproject commit 6d54f6540a661842e1af030ca0199aa6c0b4adad
diff --git a/openslides_backend/action/action.py b/openslides_backend/action/action.py
index 53f9de661e..fca7057fba 100644
--- a/openslides_backend/action/action.py
+++ b/openslides_backend/action/action.py
@@ -436,8 +436,11 @@ def _build_write_request(
# sort events: create - update - delete
events_by_type: dict[EventType, list[Event]] = defaultdict(list)
for event in self.events:
- self.apply_event(event)
- events_by_type[event["type"]].append(event)
+ if event["type"] == "delete" or not self.is_to_be_deleted(
+ event["fqid"]
+ ):
+ self.apply_event(event)
+ events_by_type[event["type"]].append(event)
information = self.get_full_history_information()
if self.is_sub_call:
write_request.information = information
@@ -765,6 +768,9 @@ def get_post_edit_function(self) -> EditFunction | None:
"""
return None
+ def is_to_be_deleted(self, fqid: FullQualifiedId) -> bool:
+ return self.datastore.is_to_be_deleted(fqid)
+
def merge_history_informations(
a: HistoryInformation | None, *other: HistoryInformation | None
diff --git a/openslides_backend/action/actions/__init__.py b/openslides_backend/action/actions/__init__.py
index cc8ecbe8ed..0604765ddf 100644
--- a/openslides_backend/action/actions/__init__.py
+++ b/openslides_backend/action/actions/__init__.py
@@ -40,6 +40,7 @@ def prepare_actions_map() -> None:
point_of_order_category,
poll,
poll_ballot,
+ poll_ballot_user,
poll_config_approval,
poll_config_rating_approval,
poll_config_rating_score,
diff --git a/openslides_backend/action/actions/meeting_user/update.py b/openslides_backend/action/actions/meeting_user/update.py
index a3ef6a3438..deb59b733b 100644
--- a/openslides_backend/action/actions/meeting_user/update.py
+++ b/openslides_backend/action/actions/meeting_user/update.py
@@ -34,6 +34,8 @@ class MeetingUserUpdate(
"motion_supporter_ids",
"motion_submitter_ids",
"chat_message_ids",
+ "acting_ballot_ids",
+ "represented_ballot_ids",
]
model = MeetingUser()
@@ -42,9 +44,6 @@ class MeetingUserUpdate(
"about_me",
"group_ids",
"poll_option_ids",
- "poll_voted_ids",
- "acting_ballot_ids",
- "represented_ballot_ids",
*meeting_user_standard_fields,
*merge_fields,
],
diff --git a/openslides_backend/action/actions/poll_ballot_user/__init__.py b/openslides_backend/action/actions/poll_ballot_user/__init__.py
new file mode 100644
index 0000000000..1fc925d0d8
--- /dev/null
+++ b/openslides_backend/action/actions/poll_ballot_user/__init__.py
@@ -0,0 +1 @@
+from . import delete # noqa
diff --git a/openslides_backend/action/actions/poll_ballot_user/delete.py b/openslides_backend/action/actions/poll_ballot_user/delete.py
new file mode 100644
index 0000000000..ca70e6158f
--- /dev/null
+++ b/openslides_backend/action/actions/poll_ballot_user/delete.py
@@ -0,0 +1,15 @@
+from ....models.models import PollBallotUser
+from ...generics.delete import DeleteAction
+from ...util.action_type import ActionType
+from ...util.default_schema import DefaultSchema
+from ...util.register import register_action
+
+
+@register_action("poll_ballot_user.delete", action_type=ActionType.BACKEND_INTERNAL)
+class PollBallotUserDelete(DeleteAction):
+ """
+ Action to delete a poll_ballot_user.
+ """
+
+ model = PollBallotUser()
+ schema = DefaultSchema(PollBallotUser()).get_delete_schema()
diff --git a/openslides_backend/action/actions/user/merge_mixins.py b/openslides_backend/action/actions/user/merge_mixins.py
index 5b5f7bea20..15429f7bf3 100644
--- a/openslides_backend/action/actions/user/merge_mixins.py
+++ b/openslides_backend/action/actions/user/merge_mixins.py
@@ -230,7 +230,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
"group_ids",
"structure_level_ids",
"history_entry_ids",
- "poll_voted_ids", # throw error if conflict on same poll
"poll_option_ids", # throw error if conflict on same poll
"acting_ballot_ids", # throw error if conflict on same poll
"represented_ballot_ids", # throw error if conflict on same poll
@@ -306,7 +305,6 @@ def check_polls(self, into: PartialModel, other_models: list[PartialModel]) -> N
"group_ids",
"vote_delegations_from_ids",
"vote_delegated_to_id",
- "poll_voted_ids",
"poll_option_ids",
"acting_ballot_ids",
"represented_ballot_ids",
@@ -395,10 +393,6 @@ def check_polls(self, into: PartialModel, other_models: list[PartialModel]) -> N
ballot_poll_ids_per_user_id: dict[int, set[int]] = {}
option_poll_ids_per_user_id: dict[int, set[int]] = {}
for meeting_user in meeting_users.values():
- if poll_voted_ids := meeting_user.get("poll_voted_ids"):
- ballot_poll_ids_per_user_id.setdefault(
- meeting_user["user_id"], set()
- ).update(set(poll_voted_ids))
if len(
(o_ids := meeting_user.get("poll_option_ids", []))
+ (
@@ -416,7 +410,7 @@ def check_polls(self, into: PartialModel, other_models: list[PartialModel]) -> N
many_models = self.datastore.get_many(
[
GetManyRequest("poll_option", o_ids, ["poll_id"]),
- GetManyRequest("poll_ballot", b_ids, ["poll_id"]),
+ GetManyRequest("poll_ballot_user", b_ids, ["poll_id"]),
]
)
if o_ids:
@@ -428,7 +422,7 @@ def check_polls(self, into: PartialModel, other_models: list[PartialModel]) -> N
for option in many_models["poll_option"].values()
}
)
- ballot_data = many_models["poll_ballot"]
+ ballot_data = many_models["poll_ballot_user"]
ballot_poll_ids_per_user_id.setdefault(
meeting_user["user_id"], set()
).update({ballot["poll_id"] for ballot in ballot_data.values()})
diff --git a/openslides_backend/action/generics/delete.py b/openslides_backend/action/generics/delete.py
index b5a39361de..f8d4c6624c 100644
--- a/openslides_backend/action/generics/delete.py
+++ b/openslides_backend/action/generics/delete.py
@@ -116,6 +116,3 @@ def is_meeting_to_be_deleted(self, meeting_id: int) -> bool:
return self.datastore.is_to_be_deleted(
fqid_from_collection_and_id("meeting", meeting_id)
)
-
- def is_to_be_deleted(self, fqid: FullQualifiedId) -> bool:
- return self.datastore.is_to_be_deleted(fqid)
diff --git a/openslides_backend/models/checker.py b/openslides_backend/models/checker.py
index ecc1709225..9774905bf4 100644
--- a/openslides_backend/models/checker.py
+++ b/openslides_backend/models/checker.py
@@ -193,6 +193,7 @@ def check_timestamp(value: Any) -> bool:
"user",
"mediafile",
"poll_ballot",
+ "poll_ballot_user",
"poll_config_approval",
"poll_config_selection",
"poll_config_rating_score",
diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py
index bf17bf33df..a516c0f113 100644
--- a/openslides_backend/models/models.py
+++ b/openslides_backend/models/models.py
@@ -1518,25 +1518,14 @@ class MeetingUser(Model):
vote_delegations_from_ids = fields.RelationListField(
to={"meeting_user": "vote_delegated_to_id"}, is_view_field=True
)
- poll_voted_ids = fields.RelationListField(
- to={"poll": "voted_ids"},
- is_view_field=True,
- is_primary=True,
- write_fields=(
- "nm_meeting_user_poll_voted_ids_poll_t",
- "meeting_user_id",
- "poll_id",
- [],
- ),
- )
poll_option_ids = fields.RelationListField(
to={"poll_option": "meeting_user_id"}, is_view_field=True, is_primary=True
)
acting_ballot_ids = fields.RelationListField(
- to={"poll_ballot": "acting_meeting_user_id"}, is_view_field=True
+ to={"poll_ballot_user": "acting_meeting_user_id"}, is_view_field=True
)
represented_ballot_ids = fields.RelationListField(
- to={"poll_ballot": "represented_meeting_user_id"}, is_view_field=True
+ to={"poll_ballot_user": "represented_meeting_user_id"}, is_view_field=True
)
chat_message_ids = fields.RelationListField(
to={"chat_message": "meeting_user_id"}, is_view_field=True
@@ -2346,15 +2335,11 @@ class Poll(Model, PollModelMixin):
is_view_field=True,
is_primary=True,
)
- voted_ids = fields.RelationListField(
- to={"meeting_user": "poll_voted_ids"},
+ ballot_user_ids = fields.RelationListField(
+ to={"poll_ballot_user": "poll_id"},
+ on_delete=fields.OnDelete.CASCADE,
is_view_field=True,
- write_fields=(
- "nm_meeting_user_poll_voted_ids_poll_t",
- "poll_id",
- "meeting_user_id",
- [],
- ),
+ is_primary=True,
)
entitled_group_ids = fields.RelationListField(
to={"group": "poll_ids"},
@@ -2385,11 +2370,27 @@ class PollBallot(Model):
poll_id = fields.RelationField(
to={"poll": "ballot_ids"}, required=True, constant=True
)
+ poll_ballot_user_id = fields.RelationField(
+ to={"poll_ballot_user": "poll_ballot_id"}
+ )
+
+
+class PollBallotUser(Model):
+ collection = "poll_ballot_user"
+ verbose_name = "poll ballot user"
+
+ id = fields.IntegerField(required=True, constant=True)
+ poll_id = fields.RelationField(
+ to={"poll": "ballot_user_ids"}, required=True, constant=True
+ )
+ poll_ballot_id = fields.RelationField(
+ to={"poll_ballot": "poll_ballot_user_id"}, is_view_field=True
+ )
acting_meeting_user_id = fields.RelationField(
- to={"meeting_user": "acting_ballot_ids"}
+ to={"meeting_user": "acting_ballot_ids"}, required=True
)
represented_meeting_user_id = fields.RelationField(
- to={"meeting_user": "represented_ballot_ids"}
+ to={"meeting_user": "represented_ballot_ids"}, required=True
)
diff --git a/openslides_backend/shared/export_helper.py b/openslides_backend/shared/export_helper.py
index d24f97f2a7..5484a2b9f8 100644
--- a/openslides_backend/shared/export_helper.py
+++ b/openslides_backend/shared/export_helper.py
@@ -212,7 +212,7 @@ def export_meeting(
if polls := export.get("poll"):
related_collections: dict[str, list[int]] = {}
for poll in polls.values():
- for collection_base_name in ["ballot", "option"]:
+ for collection_base_name in ["ballot", "ballot_user", "option"]:
if ids := poll.get(f"{collection_base_name}_ids"):
related_collections.setdefault(
f"poll_{collection_base_name}", list()
diff --git a/tests/system/action/meeting/test_clone.py b/tests/system/action/meeting/test_clone.py
index b924f43647..9adb1061c0 100644
--- a/tests/system/action/meeting/test_clone.py
+++ b/tests/system/action/meeting/test_clone.py
@@ -1979,6 +1979,10 @@ def test_clone_vote_delegated_vote(self) -> None:
"onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID
},
f"poll_ballot/{poll_id}": {
+ "poll_id": poll_id,
+ "poll_ballot_user_id": poll_id,
+ },
+ f"poll_ballot_user/{poll_id}": {
"acting_meeting_user_id": 1 if poll_id == 1 else 2,
"represented_meeting_user_id": 1 if poll_id == 1 else 2,
"poll_id": poll_id,
@@ -1989,10 +1993,15 @@ def test_clone_vote_delegated_vote(self) -> None:
self.assert_status_code(response, 200)
self.assert_model_exists(
"poll_ballot/5",
+ {"poll_id": 5, "poll_ballot_user_id": 5},
+ )
+ self.assert_model_exists(
+ "poll_ballot_user/5",
{
"acting_meeting_user_id": 3,
"represented_meeting_user_id": 3,
"poll_id": 5,
+ "poll_ballot_id": 5,
},
)
self.assert_model_exists(
diff --git a/tests/system/action/meeting/test_delete.py b/tests/system/action/meeting/test_delete.py
index d0a7795f16..6da7356058 100644
--- a/tests/system/action/meeting/test_delete.py
+++ b/tests/system/action/meeting/test_delete.py
@@ -445,6 +445,7 @@ def test_delete_full_meeting(self) -> None:
self.assert_model_not_exists(f"poll_option/{i+1}")
for i in range(9):
self.assert_model_not_exists(f"poll_ballot/{i+1}")
+ self.assert_model_not_exists("poll_ballot_user/1")
for i in range(2):
self.assert_model_not_exists(f"poll_config_approval/{i+1}")
self.assert_model_not_exists("poll_config_selection/1")
diff --git a/tests/system/action/meeting/test_import.py b/tests/system/action/meeting/test_import.py
index 3d8d15efdb..7a280e9b84 100644
--- a/tests/system/action/meeting/test_import.py
+++ b/tests/system/action/meeting/test_import.py
@@ -2397,9 +2397,13 @@ def prepare_user_with_ballot_data(self) -> dict[str, dict[str, Any]]:
{
**self.get_assignment_poll_data(3),
"poll_ballot": {
- "7": {
- "id": 7,
+ "7": {"id": 7, "poll_id": 3, "poll_ballot_user_id": 14},
+ },
+ "poll_ballot_user": {
+ "14": {
+ "id": 14,
"poll_id": 3,
+ "poll_ballot_id": 7,
"acting_meeting_user_id": 11,
"represented_meeting_user_id": 11,
},
@@ -2418,9 +2422,8 @@ def prepare_user_with_ballot_data(self) -> dict[str, dict[str, Any]]:
},
"meeting_user": {
"11": {
- "poll_voted_ids": [3],
- "acting_ballot_ids": [7],
- "represented_ballot_ids": [7],
+ "acting_ballot_ids": [14],
+ "represented_ballot_ids": [14],
}
},
}
@@ -2429,7 +2432,7 @@ def prepare_user_with_ballot_data(self) -> dict[str, dict[str, Any]]:
{
"entitled_group_ids": [2],
"ballot_ids": [7],
- "voted_ids": [11],
+ "ballot_user_ids": [14],
}
)
return data
@@ -2451,11 +2454,14 @@ def test_import_new_user_with_ballot(self) -> None:
"onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID,
},
"poll_ballot/1": {
+ "poll_id": 1,
+ "poll_ballot_user_id": 1,
+ },
+ "poll_ballot_user/1": {
"acting_meeting_user_id": 1,
"represented_meeting_user_id": 1,
"poll_id": 1,
},
- "meeting_user/1": {"poll_voted_ids": [1]},
}
)
@@ -2483,7 +2489,6 @@ def test_import_new_user_with_ballot(self) -> None:
"meeting_id": 1,
"acting_ballot_ids": [1],
"represented_ballot_ids": [1],
- "poll_voted_ids": [1],
},
)
self.assert_model_exists(
@@ -2493,7 +2498,6 @@ def test_import_new_user_with_ballot(self) -> None:
"meeting_id": 2,
"acting_ballot_ids": [2],
"represented_ballot_ids": [2],
- "poll_voted_ids": [2],
},
)
self.assert_model_exists(
@@ -2503,7 +2507,6 @@ def test_import_new_user_with_ballot(self) -> None:
"meeting_id": 2,
"acting_ballot_ids": None,
"represented_ballot_ids": None,
- "poll_voted_ids": None,
},
)
self.assert_model_exists(
@@ -2513,7 +2516,7 @@ def test_import_new_user_with_ballot(self) -> None:
"content_object_id": "assignment/1",
"meeting_id": 2,
"ballot_ids": [2],
- "voted_ids": [2],
+ "ballot_user_ids": [2],
"entitled_group_ids": [5],
},
)
diff --git a/tests/system/action/meeting_user/test_update.py b/tests/system/action/meeting_user/test_update.py
index 91d6d31adc..d292e9622d 100644
--- a/tests/system/action/meeting_user/test_update.py
+++ b/tests/system/action/meeting_user/test_update.py
@@ -150,11 +150,19 @@ def test_update_with_poll_related_fields(self) -> None:
"onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID
},
"poll_ballot/1": {
+ "poll_id": 1,
+ "poll_ballot_user_id": 1,
+ },
+ "poll_ballot_user/1": {
"poll_id": 1,
"acting_meeting_user_id": 1,
"represented_meeting_user_id": 1,
},
"poll_ballot/2": {
+ "poll_id": 1,
+ "poll_ballot_user_id": 2,
+ },
+ "poll_ballot_user/2": {
"poll_id": 1,
"acting_meeting_user_id": 1,
"represented_meeting_user_id": 2,
@@ -166,7 +174,6 @@ def test_update_with_poll_related_fields(self) -> None:
{
"id": 3,
"poll_option_ids": [1],
- "poll_voted_ids": [1],
"acting_ballot_ids": [1],
"represented_ballot_ids": [2],
},
@@ -176,14 +183,22 @@ def test_update_with_poll_related_fields(self) -> None:
expected: dict[str, dict[str, Any]] = {
"meeting_user/3": {
"poll_option_ids": [1],
- "poll_voted_ids": [1],
"acting_ballot_ids": [1],
"represented_ballot_ids": [2],
},
- "poll/1": {"voted_ids": [3]},
+ "poll/1": {"ballot_user_ids": [1, 2]},
"poll_option/1": {"meeting_user_id": 3},
- "poll_ballot/1": {"acting_meeting_user_id": 3},
- "poll_ballot/2": {"represented_meeting_user_id": 3},
+ "poll_ballot_user/1": {
+ "poll_id": 1,
+ "acting_meeting_user_id": 3,
+ "represented_meeting_user_id": 1,
+ },
+ "poll_ballot_user/2": {
+ "poll_id": 1,
+ "poll_ballot_id": 2,
+ "acting_meeting_user_id": 1,
+ "represented_meeting_user_id": 3,
+ },
}
for fqid, model in expected.items():
self.assert_model_exists(fqid, model)
diff --git a/tests/system/action/user/test_merge_together.py b/tests/system/action/user/test_merge_together.py
index 8e1e269564..b3e24fb064 100644
--- a/tests/system/action/user/test_merge_together.py
+++ b/tests/system/action/user/test_merge_together.py
@@ -1084,37 +1084,66 @@ def create_polls_with_correct_votes(self) -> None:
self.set_up_polls_for_merge()
self.set_models(
{
- "meeting_user/12": {"poll_voted_ids": [2]},
- "meeting_user/14": {"poll_voted_ids": [1]},
- "meeting_user/15": {"poll_voted_ids": [1]},
- "meeting_user/43": {"poll_voted_ids": [5]},
- "meeting_user/73": {"poll_voted_ids": [6]},
- "poll_ballot/1": {
+ "poll_ballot/11": {
"value": "no",
+ "poll_id": 1,
+ "poll_ballot_user_id": 1,
+ },
+ "poll_ballot_user/1": {
"poll_id": 1,
"acting_meeting_user_id": 14,
"represented_meeting_user_id": 14,
},
- "poll_ballot/2": {
+ "poll_ballot/12": {
"value": "no",
+ "poll_id": 1,
+ "poll_ballot_user_id": 2,
+ },
+ "poll_ballot_user/2": {
"poll_id": 1,
"acting_meeting_user_id": 14,
"represented_meeting_user_id": 15,
},
- "poll_ballot/3": {
+ "poll_ballot/13": {
"value": "4",
+ "poll_id": 2,
+ "poll_ballot_user_id": 3,
+ },
+ "poll_ballot_user/3": {
"poll_id": 2,
"acting_meeting_user_id": 12,
"represented_meeting_user_id": 12,
},
- "poll_ballot/4": {
+ "poll_ballot/14": {
"value": "abstain",
+ "poll_id": 5,
+ "poll_ballot_user_id": 4,
+ },
+ "poll_ballot_user/4": {
"poll_id": 5,
"acting_meeting_user_id": 43,
"represented_meeting_user_id": 43,
},
- "poll_ballot/5": {"value": "9", "poll_id": 6},
- "poll_ballot/6": {"value": "10", "poll_id": 6},
+ "group/7": {
+ "meeting_user_ids": [75],
+ },
+ "meeting_user/75": {
+ "user_id": 5,
+ "meeting_id": 7,
+ "vote_weight": Decimal("1"),
+ },
+ "poll_ballot/15": {"value": "9", "poll_id": 6},
+ "poll_ballot_user/5": {
+ "poll_id": 6,
+ "acting_meeting_user_id": 75,
+ "represented_meeting_user_id": 75,
+ },
+ "poll_ballot/16": {"value": "10", "poll_id": 6},
+ "poll_ballot_user/6": {
+ "poll_id": 6,
+ "acting_meeting_user_id": 73,
+ "represented_meeting_user_id": 73,
+ },
}
)
@@ -1135,7 +1164,7 @@ def assert_merge_with_polls_correct(
},
)
self.assert_model_exists("committee/60", {"user_ids": [2, 5]})
- self.assert_model_exists("committee/66", {"user_ids": [2]})
+ self.assert_model_exists("committee/66", {"user_ids": [2, 5]})
for id_ in range(3, 5):
self.assert_model_not_exists(f"user/{id_}")
for id_ in [43, 73, 14, 44, 74, *range(106, 106 + add_to_creatable_ids)]:
@@ -1146,7 +1175,6 @@ def assert_merge_with_polls_correct(
"user_id": 2,
"meeting_id": 1,
"poll_option_ids": [1, 4, 5, 7],
- "poll_voted_ids": [1, 2],
"acting_ballot_ids": [1, 2, 3],
"represented_ballot_ids": [1, 3],
},
@@ -1157,14 +1185,13 @@ def assert_merge_with_polls_correct(
"user_id": 2,
"meeting_id": 4,
"motion_submitter_ids": [1],
- "poll_voted_ids": [5],
"acting_ballot_ids": [4],
"represented_ballot_ids": [4],
},
)
self.assert_model_exists(
f"meeting_user/{106 + add_to_creatable_ids}",
- {"user_id": 2, "meeting_id": 7, "poll_voted_ids": [6]},
+ {"user_id": 2, "meeting_id": 7},
)
self.assert_model_not_exists("motion_submitter/2")
self.assert_model_exists(
@@ -1177,34 +1204,67 @@ def assert_merge_with_polls_correct(
)
self.assert_model_exists(
- "poll_ballot/2",
+ "poll_ballot_user/2",
{"acting_meeting_user_id": 12, "represented_meeting_user_id": 15},
)
for id_ in [1, 3]:
self.assert_model_exists(
- f"poll_ballot/{id_}",
+ f"poll_ballot_user/{id_}",
{"acting_meeting_user_id": 12, "represented_meeting_user_id": 12},
)
self.assert_model_exists(
- "poll_ballot/4",
+ "poll_ballot_user/4",
{"acting_meeting_user_id": 42, "represented_meeting_user_id": 42},
)
for id_ in [5, 6]: # anonymous ballots
self.assert_model_exists(
- f"poll_ballot/{id_}",
+ f"poll_ballot/{10 + id_}",
{
+ "poll_id": 6,
"value": str(id_ + 4),
- "acting_meeting_user_id": None,
- "represented_meeting_user_id": None,
+ "poll_ballot_user_id": None,
},
)
+ self.assert_model_exists(
+ "poll_ballot_user/5",
+ {
+ "poll_id": 6,
+ "poll_ballot_id": None,
+ "acting_meeting_user_id": 75,
+ "represented_meeting_user_id": 75,
+ },
+ )
+ self.assert_model_exists(
+ "poll_ballot_user/6",
+ {
+ "poll_id": 6,
+ "poll_ballot_id": None,
+ "acting_meeting_user_id": 106 + add_to_creatable_ids,
+ "represented_meeting_user_id": 106 + add_to_creatable_ids,
+ },
+ )
- self.assert_model_exists("poll/1", {"voted_ids": [12, 15]})
- self.assert_model_exists("poll/2", {"voted_ids": [12]})
+ self.assert_model_exists(
+ "poll/1",
+ {"ballot_ids": [11, 12], "ballot_user_ids": [1, 2]},
+ )
+ self.assert_model_exists(
+ "poll/2",
+ {"ballot_ids": [13], "ballot_user_ids": [3]},
+ )
for id_ in [3, 4]:
- self.assert_model_exists(f"poll/{id_}", {"voted_ids": None})
- self.assert_model_exists("poll/5", {"voted_ids": [42]})
- self.assert_model_exists("poll/6", {"voted_ids": [106 + add_to_creatable_ids]})
+ self.assert_model_exists(
+ f"poll/{id_}",
+ {"ballot_ids": None, "ballot_user_ids": None},
+ )
+ self.assert_model_exists(
+ "poll/5",
+ {"ballot_ids": [14], "ballot_user_ids": [4]},
+ )
+ self.assert_model_exists(
+ "poll/6",
+ {"ballot_ids": [15, 16], "ballot_user_ids": [5, 6]},
+ )
def test_merge_with_polls_correct(self) -> None:
password = self.assert_model_exists("user/2")["password"]
@@ -1227,37 +1287,66 @@ def test_merge_with_polls_all_errors(self) -> None:
self.set_models(
{
"poll/4": {"state": Poll.STATE_STARTED},
- "meeting_user/12": {"poll_voted_ids": [2]},
- "meeting_user/14": {"poll_voted_ids": [1]},
- "meeting_user/15": {"poll_voted_ids": [1]},
- "meeting_user/43": {"poll_voted_ids": [5]},
- "meeting_user/73": {"poll_voted_ids": [6]},
- "poll_ballot/1": {
+ "poll_ballot/11": {
"value": "no",
+ "poll_id": 1,
+ "poll_ballot_user_id": 1,
+ },
+ "poll_ballot_user/1": {
"poll_id": 1,
"acting_meeting_user_id": 14,
"represented_meeting_user_id": 14,
},
- "poll_ballot/2": {
+ "poll_ballot/12": {
"value": "no",
+ "poll_id": 1,
+ "poll_ballot_user_id": 2,
+ },
+ "poll_ballot_user/2": {
"poll_id": 1,
"acting_meeting_user_id": 14,
"represented_meeting_user_id": 15,
},
- "poll_ballot/3": {
+ "poll_ballot/13": {
"value": "4",
+ "poll_id": 2,
+ "poll_ballot_user_id": 3,
+ },
+ "poll_ballot_user/3": {
"poll_id": 2,
"acting_meeting_user_id": 12,
"represented_meeting_user_id": 12,
},
- "poll_ballot/4": {
+ "poll_ballot/14": {
"value": "abstain",
+ "poll_id": 5,
+ "poll_ballot_user_id": 4,
+ },
+ "poll_ballot_user/4": {
"poll_id": 5,
"acting_meeting_user_id": 43,
"represented_meeting_user_id": 43,
},
- "poll_ballot/5": {"value": "9", "poll_id": 6},
- "poll_ballot/6": {"value": "10", "poll_id": 6},
+ "group/7": {
+ "meeting_user_ids": [75],
+ },
+ "meeting_user/75": {
+ "user_id": 5,
+ "meeting_id": 7,
+ "vote_weight": Decimal("1"),
+ },
+ "poll_ballot/15": {"value": "9", "poll_id": 6},
+ "poll_ballot_user/5": {
+ "poll_id": 6,
+ "acting_meeting_user_id": 75,
+ "represented_meeting_user_id": 75,
+ },
+ "poll_ballot/16": {"value": "10", "poll_id": 6},
+ "poll_ballot_user/6": {
+ "poll_id": 6,
+ "acting_meeting_user_id": 73,
+ "represented_meeting_user_id": 73,
+ },
}
)
response = self.request("user.merge_together", {"id": 2, "user_ids": [3, 4, 5]})
@@ -1269,7 +1358,7 @@ def test_merge_with_polls_all_errors(self) -> None:
"some of the users are entitled to vote in currently running polls in meeting(s) 1",
"some of the selected users have different delegations roles in meeting(s) 1",
"some of the selected users are delegating votes to each other in meeting(s) 1",
- "among the selected users multiple voted in poll(s) 1",
+ "among the selected users multiple voted in poll(s) 1, 6",
"multiple of the selected users are among the options in poll(s) 1, 2, 3, 4",
]
)
diff --git a/tests/system/presenter/test_check_database.py b/tests/system/presenter/test_check_database.py
index b4396de66e..b710571ae6 100644
--- a/tests/system/presenter/test_check_database.py
+++ b/tests/system/presenter/test_check_database.py
@@ -533,6 +533,10 @@ def test_correct_relations(self) -> None:
"onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID
},
"poll_ballot/8": {
+ "poll_id": 7,
+ "poll_ballot_user_id": 12,
+ },
+ "poll_ballot_user/12": {
"poll_id": 7,
"acting_meeting_user_id": 14,
"represented_meeting_user_id": 15,
diff --git a/tests/system/presenter/test_check_database_all.py b/tests/system/presenter/test_check_database_all.py
index d62a8a4af7..b6d7fe884e 100644
--- a/tests/system/presenter/test_check_database_all.py
+++ b/tests/system/presenter/test_check_database_all.py
@@ -609,6 +609,10 @@ def test_correct_relations(self) -> None:
"onehundred_percent_base": Poll.ONEHUNDRED_PERCENT_BASE_VALID
},
"poll_ballot/8": {
+ "poll_id": 7,
+ "poll_ballot_user_id": 12,
+ },
+ "poll_ballot_user/12": {
"poll_id": 7,
"acting_meeting_user_id": 14,
"represented_meeting_user_id": 15,
diff --git a/tests/system/presenter/test_export_meeting.py b/tests/system/presenter/test_export_meeting.py
index b7ff6c871d..317f47418e 100644
--- a/tests/system/presenter/test_export_meeting.py
+++ b/tests/system/presenter/test_export_meeting.py
@@ -246,7 +246,6 @@ def test_export_meeting_find_special_users(self) -> None:
"meeting_user/114": {
"meeting_id": 1,
"user_id": 14,
- "poll_voted_ids": [80],
},
"poll/80": {
"title": "Poll 80",
@@ -263,6 +262,10 @@ def test_export_meeting_find_special_users(self) -> None:
"poll_ballot/120": {
"poll_id": 80,
"value": "yes",
+ "poll_ballot_user_id": 130,
+ },
+ "poll_ballot_user/130": {
+ "poll_id": 80,
"represented_meeting_user_id": 114,
"acting_meeting_user_id": 114,
},
@@ -282,12 +285,12 @@ def test_export_meeting_find_special_users(self) -> None:
assert data["poll_option"]["100"]["meeting_user_id"] == 113
assert data["meeting_user"]["113"]["poll_option_ids"] == [100]
- assert data["poll"]["80"]["voted_ids"] == [114]
- assert data["meeting_user"]["114"]["poll_voted_ids"] == [80]
- assert data["poll_ballot"]["120"]["acting_meeting_user_id"] == 114
- assert data["poll_ballot"]["120"]["represented_meeting_user_id"] == 114
- assert data["meeting_user"]["114"]["acting_ballot_ids"] == [120]
- assert data["meeting_user"]["114"]["represented_ballot_ids"] == [120]
+ assert data["poll"]["80"]["ballot_ids"] == [120]
+ assert data["poll"]["80"]["ballot_user_ids"] == [130]
+ assert data["poll_ballot_user"]["130"]["acting_meeting_user_id"] == 114
+ assert data["poll_ballot_user"]["130"]["represented_meeting_user_id"] == 114
+ assert data["meeting_user"]["114"]["acting_ballot_ids"] == [130]
+ assert data["meeting_user"]["114"]["represented_ballot_ids"] == [130]
def create_assignment(self, meeting_id: int, base: int) -> None:
self.set_models(
From f16270f1c1584de70ba894dd01c9845d19c28046 Mon Sep 17 00:00:00 2001
From: Viktoriia Krasnovyd <114735598+vkrasnovyd@users.noreply.github.com>
Date: Mon, 20 Jul 2026 15:19:16 +0200
Subject: [PATCH 23/23] Vote adapter cleanup (#3680)
---
.github/workflows/continuous_integration.yml | 7 -------
dev/dc.local.yml | 7 +------
dev/docker-compose.dev.yml | 21 --------------------
openslides_backend/shared/env.py | 6 +-----
tests/conftest.py | 2 +-
5 files changed, 3 insertions(+), 40 deletions(-)
diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml
index 7a4f03eaf8..fbea962e87 100644
--- a/.github/workflows/continuous_integration.yml
+++ b/.github/workflows/continuous_integration.yml
@@ -89,13 +89,6 @@ jobs:
ref: ${{ env.AUTH_COMMIT_HASH }}
path: openslides-auth-service
- - name: Clone vote service
- uses: actions/checkout@v7
- with:
- repository: OpenSlides/openslides-vote-service
- ref: main
- path: openslides-vote-service
-
- name: Run local setup
working-directory: openslides-backend
run: dev/run-local.sh
diff --git a/dev/dc.local.yml b/dev/dc.local.yml
index cc50de7cf4..23dc2f54b9 100644
--- a/dev/dc.local.yml
+++ b/dev/dc.local.yml
@@ -1,4 +1,4 @@
-# Use local auth and vote service
+# Use local auth service
version: "3"
services:
backend:
@@ -12,11 +12,6 @@ services:
environment:
- PYTHONPATH=/app:/osauthlib
- MYPYPATH=/app:/osauthlib
- vote:
- build:
- context: ../../openslides-vote-service
- args:
- CONTEXT: "prod"
auth:
build:
context: ../../openslides-auth-service
diff --git a/dev/docker-compose.dev.yml b/dev/docker-compose.dev.yml
index 1b9515dfc4..fb01acef09 100644
--- a/dev/docker-compose.dev.yml
+++ b/dev/docker-compose.dev.yml
@@ -28,7 +28,6 @@ services:
depends_on:
- postgres
- auth
- - vote
auth:
build:
context: "https://github.com/OpenSlides/openslides-auth-service.git#${COMPOSE_REFERENCE_BRANCH:-main}"
@@ -48,26 +47,6 @@ services:
- CACHE_HOST=redis
depends_on:
- redis
- vote:
- build:
- context: "https://github.com/OpenSlides/openslides-vote-service.git#feature/vote" # TODO: change branch back to the main
- target: "dev"
- args:
- CONTEXT: "dev"
- image: openslides-vote-dev
- ports:
- - "9013:9013"
- environment:
- - OPENSLIDES_DEVELOPMENT=1
- - MESSAGING=redis
- - AUTH_HOST=auth
- - DATABASE_HOST=postgres
- - VOTE_DATABASE_HOST=postgres
- - MESSAGE_BUS_HOST=redis
- - CACHE_HOST=redis
- depends_on:
- - redis
- - auth
postgres:
image: postgres:17
command: ["postgres", "-c", "log_statement=all"]
diff --git a/openslides_backend/shared/env.py b/openslides_backend/shared/env.py
index 4c897fd911..faf2ab4385 100644
--- a/openslides_backend/shared/env.py
+++ b/openslides_backend/shared/env.py
@@ -47,10 +47,6 @@ class Environment(Env):
"OPENSLIDES_LOGLEVEL": Loglevel.NOTSET.name,
"OPENTELEMETRY_ENABLED": "false",
"PRESENTER_PORT": "9003",
- "VOTE_HOST": "vote",
- "VOTE_PATH": "/system/vote",
- "VOTE_PORT": "9013",
- "VOTE_PROTOCOL": "http",
"DATABASE_HOST": "",
"DATABASE_PORT": "5432",
"DATABASE_NAME": "openslides",
@@ -104,7 +100,7 @@ def get_loglevel(self) -> str:
def get_service_url(self) -> dict[str, str]:
service_url = {}
# Extend the vars attribute with the lower case properties for the service URLs.
- for service in ("media", "vote"):
+ for service in ("media",):
key = service + "_url"
service_url[key] = self.get_endpoint(service.upper())
return service_url
diff --git a/tests/conftest.py b/tests/conftest.py
index 8c64861f67..6d3f5f25c0 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -37,7 +37,7 @@ def get_rel_db_table_names(curs: Cursor[DictRow]) -> list[str]:
gets the table names of the relational schema currently applied to the database
"""
rows = curs.execute(
- "SELECT schemaname, tablename from pg_tables where schemaname in ('public', 'vote');"
+ "SELECT schemaname, tablename from pg_tables where schemaname in ('public', 'ballot');"
).fetchall()
return [
f"{row.get('schemaname', '')}.{row.get('tablename', '')}"