diff --git a/global/data/example-data.json b/global/data/example-data.json index 7d2024fe78..48abde0e04 100644 --- a/global/data/example-data.json +++ b/global/data/example-data.json @@ -1,5 +1,5 @@ { - "_migration_index": 49, + "_migration_index": 50, "organization": { "1": { "id": 1, diff --git a/global/data/initial-data.json b/global/data/initial-data.json index 3b2dae1202..9ac2ffc52f 100644 --- a/global/data/initial-data.json +++ b/global/data/initial-data.json @@ -1,5 +1,5 @@ { - "_migration_index": 49, + "_migration_index": 50, "organization": { "1": { "id": 1, diff --git a/global/meta b/global/meta index 61273b112d..a3a876d483 160000 --- a/global/meta +++ b/global/meta @@ -1 +1 @@ -Subproject commit 61273b112d7805188bada1731e27c343406d9ef0 +Subproject commit a3a876d483d835d69e60445c910b365297d9739a diff --git a/openslides_backend/action/actions/meeting_user/history_mixin.py b/openslides_backend/action/actions/meeting_user/history_mixin.py index f46c86f082..ef9144aaf3 100644 --- a/openslides_backend/action/actions/meeting_user/history_mixin.py +++ b/openslides_backend/action/actions/meeting_user/history_mixin.py @@ -61,7 +61,7 @@ def get_history_information(self) -> HistoryInformation | None: group_information: list[str] = [] if added and removed: group_information.append("Groups changed") - else: + elif len(instance_group_ids) != 0: if added: group_information.append("Participant added to") else: diff --git a/openslides_backend/action/actions/user/update.py b/openslides_backend/action/actions/user/update.py index f754f93d18..bcab0b674f 100644 --- a/openslides_backend/action/actions/user/update.py +++ b/openslides_backend/action/actions/user/update.py @@ -4,12 +4,14 @@ from ....models.models import User from ....permissions.management_levels import OrganizationManagementLevel from ....shared.exceptions import ActionException, PermissionException +from ....shared.filters import And, FilterOperator from ....shared.patterns import fqid_from_collection_and_id from ....shared.schema import optional_id_schema from ...generics.update import UpdateAction from ...mixins.send_email_mixin import EmailCheckMixin from ...util.default_schema import DefaultSchema from ...util.register import register_action +from ..meeting_user.delete import MeetingUserDelete from .conditional_speaker_cascade_mixin import ConditionalSpeakerCascadeMixin from .create_update_permissions_mixin import CreateUpdatePermissionsMixin from .user_mixins import ( @@ -63,7 +65,20 @@ class UserUpdate( check_email_field = "email" def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: + removed_meeting_id = self.get_removed_meeting_id(instance) instance = super().update_instance(instance) + if removed_meeting_id: + meeting_users = self.datastore.filter( + "meeting_user", + And( + FilterOperator("user_id", "=", instance["id"]), + FilterOperator("meeting_id", "=", removed_meeting_id), + ), + [], + ) + self.execute_other_action( + MeetingUserDelete, [{"id": id_} for id_ in meeting_users] + ) user = self.datastore.get( fqid_from_collection_and_id("user", instance["id"]), mapped_fields=[ diff --git a/openslides_backend/migrations/migrations/0048_fix_broken_meeting_clones.py b/openslides_backend/migrations/migrations/0048_fix_broken_meeting_clones.py index 48e185b538..344ae3ed54 100644 --- a/openslides_backend/migrations/migrations/0048_fix_broken_meeting_clones.py +++ b/openslides_backend/migrations/migrations/0048_fix_broken_meeting_clones.py @@ -10,7 +10,6 @@ class Migration(BaseModelMigration): """ target_migration_index = 49 - fields = ["set_workflow_timestamp", "allow_motion_forwarding"] def migrate_models(self) -> list[BaseRequestEvent] | None: events: list[BaseRequestEvent] = [] diff --git a/openslides_backend/migrations/migrations/0049_delete_removed_meeting_users.py b/openslides_backend/migrations/migrations/0049_delete_removed_meeting_users.py new file mode 100644 index 0000000000..0e9c1d7c8a --- /dev/null +++ b/openslides_backend/migrations/migrations/0049_delete_removed_meeting_users.py @@ -0,0 +1,165 @@ +from collections import defaultdict +from typing import Any, Dict, List, Optional, Tuple, TypedDict, Union + +from datastore.migrations import BaseModelMigration, MigrationException +from datastore.shared.typing import JSON +from datastore.shared.util import fqid_from_collection_and_id +from datastore.writer.core import ( + BaseRequestEvent, + RequestDeleteEvent, + RequestUpdateEvent, +) + +from openslides_backend.shared.patterns import collection_and_id_from_fqid + +FieldTargetRemoveType = Tuple[ + str, Union[str, List[str]], Union[str, List["FieldTargetRemoveType"]] +] + +ListUpdatesDict = Dict[str, List[Union[str, int]]] +ListFieldsData = TypedDict( + "ListFieldsData", + {"add": ListUpdatesDict, "remove": ListUpdatesDict}, + total=False, +) + + +class Migration(BaseModelMigration): + """ + This migration deletes all meeting users that don't have group_ids, as that is what the update function will do as well from now on. + """ + + target_migration_index = 50 + field_target_relation_list: List[FieldTargetRemoveType] = [ + ("assignment_candidate_ids", "assignment_candidate", "meeting_user_id"), + ("chat_message_ids", "chat_message", "meeting_user_id"), + ("meeting_id", "meeting", "meeting_user_ids"), + ( + "motion_submitter_ids", + "motion_submitter", + [ + ("meeting_id", "meeting", "motion_submitter_ids"), + ("motion_id", "motion", "submitter_ids"), + ], + ), # CASCADE + ( + "personal_note_ids", + "personal_note", + [ + ("content_object_id", ["motion"], "personal_note_ids"), # GENERIC + ("meeting_id", "meeting", "personal_note_ids"), + ], + ), # CASCADE + ("speaker_ids", "speaker", "meeting_user_id"), + ("supported_motion_ids", "motion", "supporter_meeting_user_ids"), + ("user_id", "user", "meeting_user_ids"), + ("vote_delegated_to_id", "meeting_user", "vote_delegations_from_ids"), + ("vote_delegations_from_ids", "meeting_user", "vote_delegated_to_id"), + ] + remove_events: Dict[str, BaseRequestEvent] + additional_events: Dict[str, Tuple[Dict[str, JSON], ListFieldsData]] + + def migrate_models(self) -> Optional[List[BaseRequestEvent]]: + db_models = self.reader.get_all("meeting_user") + to_be_deleted = { + id_: meeting_user + for id_, meeting_user in db_models.items() + if len(meeting_user.get("group_ids", [])) == 0 + } + self.remove_events = { + f"meeting_user/{id_}": RequestDeleteEvent( + fqid_from_collection_and_id("meeting_user", id_), + ) + for id_ in to_be_deleted.keys() + } + self.additional_events = defaultdict() + for id_, meeting_user in to_be_deleted.items(): + self.add_relation_migration_events( + id_, meeting_user, self.field_target_relation_list + ) + additional = [ + RequestUpdateEvent(fqid, fields, list_fields) + for fqid, (fields, list_fields) in self.additional_events.items() + ] + return [*additional, *self.remove_events.values()] + + def add_relation_migration_events( + self, + remove_id: int, + remove_model: Any, + instructions: List[FieldTargetRemoveType], + ) -> None: + for field, collection, to_empty in instructions: + target_ids = remove_model.get(field) + if target_ids: + if isinstance(target_ids, int) or isinstance(target_ids, str): + if isinstance(target_ids, str): # GENERICS + collection, target_ids = collection_and_id_from_fqid(target_ids) + elif not isinstance(collection, str): + raise MigrationException( + f"Couldn't resolve generic field {field}, value '{target_ids}' was not an fqid" + ) + self.add_relation_migration_events_helper( + collection, target_ids, to_empty, remove_id + ) + elif isinstance(target_ids, list): # list + for target_date in target_ids: + if isinstance(target_date, int) or isinstance(target_date, str): + if isinstance(target_date, str): # GENERICS + collection, target_date = collection_and_id_from_fqid( + target_date + ) + elif not isinstance(collection, str): + raise MigrationException( + f"Couldn't resolve generic field {field}, value '{target_ids}' was not an fqid" + ) + self.add_relation_migration_events_helper( + collection, target_date, to_empty, remove_id + ) + else: + raise MigrationException( + f"Couldn't resolve field {field} as id field, value: '{target_ids}'" + ) + else: + raise MigrationException( + f"Couldn't resolve field {field} as id field, value: '{target_ids}'" + ) + + def add_relation_migration_events_helper( + self, + collection: str, + model_id: int, + to_empty: Union[str, List["FieldTargetRemoveType"]], + remove_id: int, + ) -> None: + fqid = fqid_from_collection_and_id(collection, model_id) + if not self.remove_events.get(fqid): + if isinstance(to_empty, str): + is_list = to_empty.endswith("_ids") + if prior_event := self.additional_events.get(fqid): + if is_list: + if remove := prior_event[1].get("remove"): + if prior_empty_values := remove.get(to_empty): + remove[to_empty] = list( + set([*prior_empty_values, remove_id]) + ) + else: + remove[to_empty] = [remove_id] + else: + prior_event[1]["remove"] = {to_empty: [remove_id]} + else: + prior_event[0][to_empty] = None + else: + if is_list: + self.additional_events[fqid] = ( + {}, + {"remove": {to_empty: [remove_id]}}, + ) + else: + self.additional_events[fqid] = ({to_empty: None}, {}) + else: + if self.additional_events.get(fqid): + del self.additional_events[fqid] + self.remove_events[fqid] = RequestDeleteEvent(fqid) + model = self.reader.get(fqid) + self.add_relation_migration_events(model_id, model, to_empty) diff --git a/tests/system/action/user/test_update.py b/tests/system/action/user/test_update.py index 9b6860c88e..7472be8778 100644 --- a/tests/system/action/user/test_update.py +++ b/tests/system/action/user/test_update.py @@ -356,19 +356,18 @@ def test_committee_manager_without_committee_ids(self) -> None: "user/111", { "meeting_ids": [], - "meeting_user_ids": [1111], + "meeting_user_ids": [], "committee_management_ids": [60, 61], "committee_ids": [60, 61], }, ) - self.assert_model_exists( + self.assert_model_deleted( "meeting_user/1111", {"group_ids": [], "meta_deleted": False} ) self.assert_history_information( "user/111", [ - "Participant removed from group {} in meeting {}", - "group/600", + "Participant removed from meeting {}", "meeting/60", "Personal data changed", "Committee management changed", @@ -489,7 +488,7 @@ def test_committee_manager_add_and_remove_both(self) -> None: "committee_management_ids": [4], "meeting_ids": [22, 33], "committee_ids": [2, 3, 4], - "meeting_user_ids": [111, 112, 113], + "meeting_user_ids": [112, 113], }, ) self.assert_model_exists("committee/1", {"user_ids": []}) @@ -499,6 +498,7 @@ def test_committee_manager_add_and_remove_both(self) -> None: self.assert_model_exists("meeting/11", {"user_ids": []}) self.assert_model_exists("meeting/22", {"user_ids": [123]}) self.assert_model_exists("meeting/33", {"user_ids": [123]}) + self.assert_model_deleted("meeting_user/111") def test_update_broken_email(self) -> None: self.create_model( @@ -2092,6 +2092,7 @@ def test_group_removal_with_speaker(self) -> None: "user_id": 1234, "speaker_ids": [14, 24], "group_ids": [42], + "personal_note_ids": [444], }, "meeting_user/5555": { "meeting_id": 5, @@ -2102,6 +2103,8 @@ def test_group_removal_with_speaker(self) -> None: "meeting/4": { "is_active_in_organization_id": 1, "meeting_user_ids": [4444], + "motion_ids": [44], + "personal_note_ids": [444], "committee_id": 1, }, "meeting/5": { @@ -2109,6 +2112,15 @@ def test_group_removal_with_speaker(self) -> None: "meeting_user_ids": [5555], "committee_id": 1, }, + "motion/44": { + "meeting_id": 4, + "personal_note_ids": [444], + }, + "personal_note/444": { + "content_object_id": "motion/44", + "meeting_user_id": 4444, + "meeting_id": 4, + }, "committee/1": {"meeting_ids": [4, 5]}, "speaker/14": {"meeting_user_id": 4444, "meeting_id": 4}, "speaker/24": { @@ -2130,25 +2142,45 @@ def test_group_removal_with_speaker(self) -> None: "user/1234", { "username": "username_abcdefgh123", - "meeting_user_ids": [4444, 5555], + "meeting_user_ids": [5555], }, ) - self.assert_model_exists( - "meeting_user/4444", - {"group_ids": [], "speaker_ids": [24], "meta_deleted": False}, - ) + self.assert_model_deleted("meeting_user/4444") self.assert_model_exists( "meeting_user/5555", {"group_ids": [53], "speaker_ids": [25], "meta_deleted": False}, ) self.assert_model_exists( - "speaker/24", {"meeting_user_id": 4444, "meeting_id": 4} + "speaker/24", {"meeting_user_id": None, "meeting_id": 4} ) self.assert_model_exists( "speaker/25", {"meeting_user_id": 5555, "meeting_id": 5} ) self.assert_model_deleted("speaker/14") + response2 = self.request( + "user.update", {"id": 1234, "group_ids": [42], "meeting_id": 4} + ) + + self.assert_status_code(response2, 200) + self.assert_model_exists( + "user/1234", + { + "username": "username_abcdefgh123", + "meeting_user_ids": [5555, 5556], + }, + ) + self.assert_model_deleted("meeting_user/4444") + self.assert_model_deleted("personal_note/444") + self.assert_model_exists( + "meeting_user/5556", + { + "user_id": 1234, + "meeting_id": 4, + "group_ids": [42], + }, + ) + def test_partial_group_removal_with_speaker(self) -> None: self.set_models( { diff --git a/tests/system/migrations/test_0049_delete_removed_meeting_clones.py b/tests/system/migrations/test_0049_delete_removed_meeting_clones.py new file mode 100644 index 0000000000..44d41a58f9 --- /dev/null +++ b/tests/system/migrations/test_0049_delete_removed_meeting_clones.py @@ -0,0 +1,613 @@ +def test_migration(write, finalize, assert_model): + write( + { + "type": "create", + "fqid": "meeting/1", + "fields": { + "id": 1, + "group_ids": [1], + "meeting_user_ids": [101, 201, 301], + "motion_submitter_ids": [1011, 1021], + "personal_note_ids": [1011, 1021], + }, + }, + { + "type": "create", + "fqid": "meeting/2", + "fields": { + "id": 2, + "group_ids": [2], + "meeting_user_ids": [102, 202, 302], + "motion_submitter_ids": [2011, 2012], + "personal_note_ids": [2021, 2022], + }, + }, + { + "type": "create", + "fqid": "group/1", + "fields": { + "id": 1, + "meeting_id": 1, + "meeting_user_ids": [101, 301], + }, + }, + { + "type": "create", + "fqid": "group/2", + "fields": { + "id": 2, + "meeting_id": 2, + "meeting_user_ids": [302], + }, + }, + { + "type": "create", + "fqid": "user/10", + "fields": { + "id": 10, + "meeting_user_ids": [101, 102], + }, + }, + { + "type": "create", + "fqid": "user/20", + "fields": { + "id": 20, + "meeting_user_ids": [201, 202], + }, + }, + { + "type": "create", + "fqid": "user/30", + "fields": { + "id": 30, + "meeting_user_ids": [301, 302], + }, + }, + { + "type": "create", + "fqid": "meeting_user/101", + "fields": { + "id": 102, + "meeting_id": 1, + "group_ids": [1], + "user_id": 10, + "assignment_candidate_ids": [1011], + "chat_message_ids": [1011], + "motion_submitter_ids": [1011], + "personal_note_ids": [1011], + "speaker_ids": [1011], + "supported_motion_ids": [1], + "vote_delegations_from_ids": [201, 301], + }, + }, + { + "type": "create", + "fqid": "meeting_user/102", + "fields": { + "id": 102, + "meeting_id": 2, + "group_ids": [], + "user_id": 10, + "motion_submitter_ids": [1021], + "personal_note_ids": [1021], + "supported_motion_ids": [1, 11], + "vote_delegations_from_ids": [202, 302], + }, + }, + { + "type": "create", + "fqid": "meeting_user/201", + "fields": { + "id": 201, + "meeting_id": 1, + "group_ids": [], + "user_id": 20, + "assignment_candidate_ids": [2011, 2012], + "chat_message_ids": [2011, 2012], + "motion_submitter_ids": [2011, 2012], + "supported_motion_ids": [2, 22], + "vote_delegated_to_id": 101, + }, + }, + { + "type": "create", + "fqid": "meeting_user/202", + "fields": { + "id": 202, + "meeting_id": 2, + "group_ids": [], + "user_id": 20, + "personal_note_ids": [2021, 2022], + "speaker_ids": [2021, 2022], + "supported_motion_ids": [22], + "vote_delegated_to_id": 102, + }, + }, + { + "type": "create", + "fqid": "meeting_user/301", + "fields": { + "id": 301, + "meeting_id": 1, + "group_ids": [1], + "user_id": 30, + "vote_delegated_to_id": 101, + }, + }, + { + "type": "create", + "fqid": "meeting_user/302", + "fields": { + "id": 302, + "meeting_id": 2, + "group_ids": [2], + "user_id": 30, + "vote_delegated_to_id": 102, + }, + }, + { + "type": "create", + "fqid": "assignment_candidate/1011", + "fields": {"id": 1011, "meeting_user_id": 101}, + }, + { + "type": "create", + "fqid": "assignment_candidate/2011", + "fields": {"id": 2011, "meeting_user_id": 201}, + }, + { + "type": "create", + "fqid": "assignment_candidate/2012", + "fields": {"id": 2012, "meeting_user_id": 201}, + }, + { + "type": "create", + "fqid": "chat_message/1011", + "fields": {"id": 1011, "meeting_user_id": 101}, + }, + { + "type": "create", + "fqid": "chat_message/2011", + "fields": {"id": 2011, "meeting_user_id": 201}, + }, + { + "type": "create", + "fqid": "chat_message/2012", + "fields": {"id": 2012, "meeting_user_id": 201}, + }, + { + "type": "create", + "fqid": "motion/1", + "fields": { + "id": 1, + "submitter_ids": [1011, 1021], + "personal_note_ids": [1011, 1021], + "supporter_meeting_user_ids": [101, 102], + }, + }, + { + "type": "create", + "fqid": "motion/2", + "fields": { + "id": 2, + "submitter_ids": [2011, 2012], + "personal_note_ids": [2021, 2022], + "supporter_meeting_user_ids": [201], + }, + }, + { + "type": "create", + "fqid": "motion/11", + "fields": { + "id": 11, + "supporter_meeting_user_ids": [102], + }, + }, + { + "type": "create", + "fqid": "motion/22", + "fields": { + "id": 22, + "supporter_meeting_user_ids": [201, 202], + }, + }, + { + "type": "create", + "fqid": "motion_submitter/1011", + "fields": { + "id": 1011, + "meeting_user_id": 101, + "meeting_id": 1, + "motion_id": 1, + }, + }, + { + "type": "create", + "fqid": "motion_submitter/1021", + "fields": { + "id": 1021, + "meeting_user_id": 102, + "meeting_id": 1, + "motion_id": 1, + }, + }, + { + "type": "create", + "fqid": "motion_submitter/2011", + "fields": { + "id": 2011, + "meeting_user_id": 201, + "meeting_id": 2, + "motion_id": 2, + }, + }, + { + "type": "create", + "fqid": "motion_submitter/2012", + "fields": { + "id": 2012, + "meeting_user_id": 201, + "meeting_id": 2, + "motion_id": 2, + }, + }, + { + "type": "create", + "fqid": "personal_note/1011", + "fields": { + "id": 1011, + "meeting_user_id": 101, + "meeting_id": 1, + "content_object_id": "motion/1", + }, + }, + { + "type": "create", + "fqid": "personal_note/1021", + "fields": { + "id": 1021, + "meeting_user_id": 102, + "meeting_id": 1, + "content_object_id": "motion/1", + }, + }, + { + "type": "create", + "fqid": "personal_note/2021", + "fields": { + "id": 2021, + "meeting_user_id": 202, + "meeting_id": 2, + "content_object_id": "motion/2", + }, + }, + { + "type": "create", + "fqid": "personal_note/2022", + "fields": { + "id": 2022, + "meeting_user_id": 202, + "meeting_id": 2, + "content_object_id": "motion/2", + }, + }, + { + "type": "create", + "fqid": "speaker/1011", + "fields": { + "id": 1011, + "meeting_user_id": 101, + }, + }, + { + "type": "create", + "fqid": "speaker/2021", + "fields": { + "id": 2021, + "meeting_user_id": 202, + }, + }, + { + "type": "create", + "fqid": "speaker/2022", + "fields": { + "id": 2022, + "meeting_user_id": 202, + }, + }, + ) + + finalize("0049_delete_removed_meeting_users") + + assert_model( + "meeting_user/102", + { + "id": 102, + "meeting_id": 2, + "group_ids": [], + "user_id": 10, + "motion_submitter_ids": [1021], + "personal_note_ids": [1021], + "supported_motion_ids": [1, 11], + "vote_delegations_from_ids": [202, 302], + "meta_deleted": True, + }, + ) + assert_model( + "meeting_user/201", + { + "id": 201, + "meeting_id": 1, + "group_ids": [], + "user_id": 20, + "assignment_candidate_ids": [2011, 2012], + "chat_message_ids": [2011, 2012], + "motion_submitter_ids": [2011, 2012], + "supported_motion_ids": [2, 22], + "vote_delegated_to_id": 101, + "meta_deleted": True, + }, + ) + assert_model( + "meeting_user/202", + { + "id": 202, + "meeting_id": 2, + "group_ids": [], + "user_id": 20, + "personal_note_ids": [2021, 2022], + "speaker_ids": [2021, 2022], + "supported_motion_ids": [22], + "meta_deleted": True, + "vote_delegated_to_id": 102, + }, + ) + + assert_model( + "meeting_user/101", + { + "id": 102, + "meeting_id": 1, + "group_ids": [1], + "user_id": 10, + "assignment_candidate_ids": [1011], + "chat_message_ids": [1011], + "motion_submitter_ids": [1011], + "personal_note_ids": [1011], + "speaker_ids": [1011], + "supported_motion_ids": [1], + "vote_delegations_from_ids": [301], + }, + ) + assert_model( + "meeting_user/301", + { + "id": 301, + "meeting_id": 1, + "group_ids": [1], + "user_id": 30, + "vote_delegated_to_id": 101, + }, + ) + assert_model( + "meeting_user/302", + { + "id": 302, + "meeting_id": 2, + "group_ids": [2], + "user_id": 30, + }, + ) + assert_model("assignment_candidate/1011", {"id": 1011, "meeting_user_id": 101}) + assert_model( + "assignment_candidate/2011", + { + "id": 2011, + }, + ) + assert_model( + "assignment_candidate/2012", + { + "id": 2012, + }, + ) + assert_model("chat_message/1011", {"id": 1011, "meeting_user_id": 101}) + assert_model( + "chat_message/2011", + { + "id": 2011, + }, + ) + assert_model( + "chat_message/2012", + { + "id": 2012, + }, + ) + assert_model( + "motion_submitter/1011", + { + "id": 1011, + "meeting_user_id": 101, + "meeting_id": 1, + "motion_id": 1, + }, + ) + assert_model( + "motion_submitter/1021", + { + "id": 1021, + "meeting_user_id": 102, + "meeting_id": 1, + "motion_id": 1, + "meta_deleted": True, + }, + ) + assert_model( + "motion_submitter/2011", + { + "id": 2011, + "meeting_user_id": 201, + "meeting_id": 2, + "motion_id": 2, + "meta_deleted": True, + }, + ) + assert_model( + "motion_submitter/2012", + { + "id": 2012, + "meeting_user_id": 201, + "meeting_id": 2, + "motion_id": 2, + "meta_deleted": True, + }, + ) + assert_model( + "personal_note/1011", + { + "id": 1011, + "meeting_user_id": 101, + "meeting_id": 1, + "content_object_id": "motion/1", + }, + ) + assert_model( + "personal_note/1021", + { + "id": 1021, + "meeting_user_id": 102, + "meeting_id": 1, + "content_object_id": "motion/1", + "meta_deleted": True, + }, + ) + assert_model( + "personal_note/2021", + { + "id": 2021, + "meeting_user_id": 202, + "meeting_id": 2, + "content_object_id": "motion/2", + "meta_deleted": True, + }, + ) + assert_model( + "personal_note/2022", + { + "id": 2022, + "meeting_user_id": 202, + "meeting_id": 2, + "content_object_id": "motion/2", + "meta_deleted": True, + }, + ) + assert_model( + "speaker/1011", + { + "id": 1011, + "meeting_user_id": 101, + }, + ) + assert_model( + "speaker/2021", + { + "id": 2021, + }, + ) + assert_model( + "speaker/2022", + { + "id": 2022, + }, + ) + assert_model( + "meeting/1", + { + "id": 1, + "group_ids": [1], + "meeting_user_ids": [101, 301], + "motion_submitter_ids": [1011], + "personal_note_ids": [1011], + }, + ) + assert_model( + "meeting/2", + { + "id": 2, + "group_ids": [2], + "meeting_user_ids": [302], + "motion_submitter_ids": [], + "personal_note_ids": [], + }, + ) + assert_model( + "motion/1", + { + "id": 1, + "submitter_ids": [1011], + "personal_note_ids": [1011], + "supporter_meeting_user_ids": [101], + }, + ) + assert_model( + "motion/2", + { + "id": 2, + "submitter_ids": [], + "personal_note_ids": [], + "supporter_meeting_user_ids": [], + }, + ) + assert_model( + "motion/11", + { + "id": 11, + "supporter_meeting_user_ids": [], + }, + ) + assert_model( + "motion/22", + { + "id": 22, + "supporter_meeting_user_ids": [], + }, + ) + assert_model( + "group/1", + { + "id": 1, + "meeting_id": 1, + "meeting_user_ids": [101, 301], + }, + ) + assert_model( + "group/2", + { + "id": 2, + "meeting_id": 2, + "meeting_user_ids": [302], + }, + ) + assert_model( + "user/10", + { + "id": 10, + "meeting_user_ids": [101], + }, + ) + assert_model( + "user/20", + { + "id": 20, + "meeting_user_ids": [], + }, + ) + assert_model( + "user/30", + { + "id": 30, + "meeting_user_ids": [301, 302], + }, + )