From 55a9d554817f8597220a49ea7c0b702e7c9b89ad Mon Sep 17 00:00:00 2001 From: vkrasnovyd Date: Fri, 10 Jul 2026 15:33:25 +0200 Subject: [PATCH 1/7] Re-generate models --- meta | 2 +- openslides_backend/models/models.py | 51 ++++++++++++++++------------- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/meta b/meta index 64e82b5cc6..461432c8c9 160000 --- a/meta +++ b/meta @@ -1 +1 @@ -Subproject commit 64e82b5cc6ac00d7b43f30ce702f975a72435e09 +Subproject commit 461432c8c9dae7e5db37f5227c4830e2b225907f diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py index 89dcb0880f..529ffcd05d 100644 --- a/openslides_backend/models/models.py +++ b/openslides_backend/models/models.py @@ -1517,25 +1517,18 @@ 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"}, + on_delete=fields.OnDelete.CASCADE, + 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"}, + on_delete=fields.OnDelete.CASCADE, + is_view_field=True, ) chat_message_ids = fields.RelationListField( to={"chat_message": "meeting_user_id"}, is_view_field=True @@ -2342,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"}, @@ -2381,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 ) From bd28990ea15373736f0bf23093961642efefcf83 Mon Sep 17 00:00:00 2001 From: vkrasnovyd Date: Tue, 14 Jul 2026 15:58:19 +0200 Subject: [PATCH 2/7] Use poll_ballot_user in backend --- 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 ++ openslides_backend/action/actions/__init__.py | 1 + .../action/actions/meeting_user/update.py | 1 - .../actions/poll_ballot_user/__init__.py | 1 + .../action/actions/poll_ballot_user/delete.py | 15 ++ .../action/actions/user/merge_mixins.py | 10 +- openslides_backend/models/checker.py | 1 + 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 | 150 +++++++++++++----- tests/system/presenter/test_check_database.py | 4 + .../presenter/test_check_database_all.py | 4 + tests/system/presenter/test_export_meeting.py | 17 +- 19 files changed, 219 insertions(+), 79 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/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 f1703a83a2..823147f165 100644 --- a/openslides_backend/action/actions/meeting_user/update.py +++ b/openslides_backend/action/actions/meeting_user/update.py @@ -41,7 +41,6 @@ class MeetingUserUpdate( "about_me", "group_ids", "poll_option_ids", - "poll_voted_ids", "acting_ballot_ids", "represented_ballot_ids", *meeting_user_standard_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 ee260e07bb..7e9c82675d 100644 --- a/openslides_backend/action/actions/user/merge_mixins.py +++ b/openslides_backend/action/actions/user/merge_mixins.py @@ -229,7 +229,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: "chat_message_ids", "group_ids", "structure_level_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 @@ -305,7 +304,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", @@ -394,10 +392,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", [])) + ( @@ -415,7 +409,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: @@ -427,7 +421,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/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/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 c55de8b324..5dd075746b 100644 --- a/tests/system/action/user/test_merge_together.py +++ b/tests/system/action/user/test_merge_together.py @@ -945,37 +945,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, + }, } ) @@ -996,7 +1025,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)]: @@ -1007,7 +1036,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], }, @@ -1018,14 +1046,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( @@ -1038,34 +1065,52 @@ 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_user_ids": [1, 2]}) + self.assert_model_exists("poll/2", {"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_user_ids": None}) + self.assert_model_exists("poll/5", {"ballot_user_ids": [4]}) + self.assert_model_exists("poll/6", {"ballot_user_ids": [5, 6]}) def test_merge_with_polls_correct(self) -> None: password = self.assert_model_exists("user/2")["password"] @@ -1088,37 +1133,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]}) @@ -1130,7 +1204,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 7ae72bcfafc27851c0f9a1a943ea623a0633a8fc Mon Sep 17 00:00:00 2001 From: vkrasnovyd Date: Tue, 14 Jul 2026 17:11:09 +0200 Subject: [PATCH 3/7] Extend test data --- .../system/action/user/test_merge_together.py | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/system/action/user/test_merge_together.py b/tests/system/action/user/test_merge_together.py index e66cc26f63..7721aec91f 100644 --- a/tests/system/action/user/test_merge_together.py +++ b/tests/system/action/user/test_merge_together.py @@ -1244,12 +1244,27 @@ def assert_merge_with_polls_correct( }, ) - self.assert_model_exists("poll/1", {"ballot_user_ids": [1, 2]}) - self.assert_model_exists("poll/2", {"ballot_user_ids": [3]}) + 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_}", {"ballot_user_ids": None}) - self.assert_model_exists("poll/5", {"ballot_user_ids": [4]}) - self.assert_model_exists("poll/6", {"ballot_user_ids": [5, 6]}) + 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"] From 63dc046eb7277d9e7a0c6f1080c5c6108ee34cae Mon Sep 17 00:00:00 2001 From: vkrasnovyd Date: Wed, 15 Jul 2026 12:54:09 +0200 Subject: [PATCH 4/7] Re-generate models --- meta | 2 +- openslides_backend/models/models.py | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/meta b/meta index 0dd4dd02b2..d3954249a0 160000 --- a/meta +++ b/meta @@ -1 +1 @@ -Subproject commit 0dd4dd02b220393ade174161bbf5ee8dfbd20bad +Subproject commit d3954249a0fbe7396137df616a4869048442d83f diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py index 89580d765d..1640af114c 100644 --- a/openslides_backend/models/models.py +++ b/openslides_backend/models/models.py @@ -1522,14 +1522,10 @@ class MeetingUser(Model): to={"poll_option": "meeting_user_id"}, is_view_field=True, is_primary=True ) acting_ballot_ids = fields.RelationListField( - to={"poll_ballot_user": "acting_meeting_user_id"}, - on_delete=fields.OnDelete.CASCADE, - is_view_field=True, + to={"poll_ballot_user": "acting_meeting_user_id"}, is_view_field=True ) represented_ballot_ids = fields.RelationListField( - to={"poll_ballot_user": "represented_meeting_user_id"}, - on_delete=fields.OnDelete.CASCADE, - 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 From a3ba691da5741ea2bfadccc7ae4f39b098801ecb Mon Sep 17 00:00:00 2001 From: vkrasnovyd Date: Wed, 15 Jul 2026 12:54:32 +0200 Subject: [PATCH 5/7] Update meeting_user.update schema --- openslides_backend/action/actions/meeting_user/update.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openslides_backend/action/actions/meeting_user/update.py b/openslides_backend/action/actions/meeting_user/update.py index 00e52217c3..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,8 +44,6 @@ class MeetingUserUpdate( "about_me", "group_ids", "poll_option_ids", - "acting_ballot_ids", - "represented_ballot_ids", *meeting_user_standard_fields, *merge_fields, ], From b539aa94906dd9f88c5f4f80cad52511d868cdd2 Mon Sep 17 00:00:00 2001 From: vkrasnovyd Date: Wed, 15 Jul 2026 12:55:06 +0200 Subject: [PATCH 6/7] Update events generation --- openslides_backend/action/action.py | 10 ++++++++-- openslides_backend/action/generics/delete.py | 3 --- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/openslides_backend/action/action.py b/openslides_backend/action/action.py index f2d3e61689..4f44c780ea 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/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) From f237c5bbfeec359fe4dba7a8e28c16348f6af5aa Mon Sep 17 00:00:00 2001 From: vkrasnovyd Date: Fri, 17 Jul 2026 14:55:05 +0200 Subject: [PATCH 7/7] Update meta hash --- meta | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meta b/meta index d3954249a0..6d54f6540a 160000 --- a/meta +++ b/meta @@ -1 +1 @@ -Subproject commit d3954249a0fbe7396137df616a4869048442d83f +Subproject commit 6d54f6540a661842e1af030ca0199aa6c0b4adad