diff --git a/data/example-data.json b/data/example-data.json index a4d7b1ee34..8701836eac 100644 --- a/data/example-data.json +++ b/data/example-data.json @@ -1,5 +1,5 @@ { - "_migration_index": 74, + "_migration_index": 75, "gender":{ "1":{ "id": 1, diff --git a/data/initial-data.json b/data/initial-data.json index bb44b1cbc5..e197bca7e8 100644 --- a/data/initial-data.json +++ b/data/initial-data.json @@ -1,5 +1,5 @@ { - "_migration_index": 74, + "_migration_index": 75, "gender":{ "1":{ "id": 1, diff --git a/docs/actions/chat_message.create.md b/docs/actions/chat_message.create.md index d8dd542fa3..679f6e67e7 100644 --- a/docs/actions/chat_message.create.md +++ b/docs/actions/chat_message.create.md @@ -14,4 +14,5 @@ Creates a new `chat_message` for the `chat_group` given by the key `chat_group_i ## Permission -Every user, who is in one of the write groups of a chat group, or has the permission `chat.can_manage` can create a `chat_message`. \ No newline at end of file +Every user, who is in one of the write groups of a chat group, or has the permission `chat.can_manage` can create a `chat_message`. +User also needs to be part of the meeting. \ No newline at end of file diff --git a/docs/actions/motion.create.md b/docs/actions/motion.create.md index 72f6b348fd..c316769c39 100644 --- a/docs/actions/motion.create.md +++ b/docs/actions/motion.create.md @@ -26,7 +26,7 @@ // Optional special fields, see notes below workflow_id: Id; - submitter_ids: Id[]; + submitter_meeting_user_ids: Id[]; // Non-model fields for customizing the agenda item creation, optional agenda_create: boolean; @@ -58,7 +58,7 @@ This is the logic for other fields depending on the motion type: There are some fields that need special attention: - `workflow_id`: If it is given, the motion's state is set to the workflow's first state. The workflow must be from the same meeting. If the field is not given, one of the three default (`meeting/motions_default_workflow_id` or `meeting/motions_default_amendment_workflow_id`) workflows is used depending on the type of the motion to create. - `additional_submitter` a text field where text-based submitter information may be entered. Cannot be set unless `meeting/motions_create_enable_additional_submitter_text` is `true`. Requires permissions `Motion.CAN_CREATE` and `Motion.CAN_MANAGE_METADATA`. -- `submitter_ids`: These are **user ids** and not ids of the `motion_submitter` model. If nothing is given (`[]`) and the field `additional_submitter` isn't filled, the request user's id is used. For each id in the list a `motion_submitter` model is created. The weight must be set to the order of the given list. Requires permissions `Motion.CAN_CREATE`, `Motion.CAN_MANAGE_METADATA` and `User.CAN_SEE`. +- `submitter_meeting_user_ids`: These are ids of the meeting users that should get a `motion_submitter` model. Can be left empty. The weight of the new submitters is set to the order of the given list. Requires permissions `Motion.CAN_CREATE`, `Motion.CAN_MANAGE_METADATA` and `User.CAN_SEE` (the latter two only if not setting oneself). - `agenda_*`: See [Agenda](https://github.com/OpenSlides/OpenSlides/wiki/Agenda#additional-fields-during-creation-of-agenda-content-objects). Other things to do when creating motions: diff --git a/docs/actions/user.update.md b/docs/actions/user.update.md index 057e4353d5..4244f4c135 100644 --- a/docs/actions/user.update.md +++ b/docs/actions/user.update.md @@ -85,6 +85,7 @@ Updates a user. Note: `is_present_in_meeting_ids` is not available in update, since there is no possibility to partially update this field. This can be done via [user.set_present](user.set_present.md). If the user is removed from all groups of the meeting, all his unstarted speakers in that meeting will be deleted. +His meeting_user for that meeting will also be deleted. If the user was the last member of the meetings admin group and he happens to be removed from the latter through this action, as long as the meeting is not a template, there will be an error. diff --git a/meta b/meta index 41dea1f287..8905a6002a 160000 --- a/meta +++ b/meta @@ -1 +1 @@ -Subproject commit 41dea1f2874072c7acf432c4f22ea100969a1b00 +Subproject commit 8905a6002a05527d45b8c575a67de983c70e0c7d diff --git a/openslides_backend/action/actions/chat_message/create.py b/openslides_backend/action/actions/chat_message/create.py index a803cbc52c..8a9780e827 100644 --- a/openslides_backend/action/actions/chat_message/create.py +++ b/openslides_backend/action/actions/chat_message/create.py @@ -4,7 +4,7 @@ from ....models.models import ChatMessage from ....permissions.permission_helper import has_perm from ....permissions.permissions import Permissions -from ....shared.exceptions import PermissionDenied +from ....shared.exceptions import ActionException, PermissionDenied from ....shared.patterns import fqid_from_collection_and_id from ...mixins.create_action_with_inferred_meeting import ( CreateActionWithInferredMeeting, @@ -28,9 +28,13 @@ class ChatMessageCreate(MeetingUserHelperMixin, CreateActionWithInferredMeeting) def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: instance = super().update_instance(instance) - instance["meeting_user_id"] = self.create_or_get_meeting_user( - instance["meeting_id"], self.user_id - ) + instance["meeting_user_id"] = ( + self.get_meeting_user(instance["meeting_id"], self.user_id, ["id"]) or {} + ).get("id") + if not instance.get("meeting_user_id"): + raise ActionException( + "Cannot create chat message: You are not a participant of the meeting." + ) instance["created"] = round(time()) return instance diff --git a/openslides_backend/action/actions/meeting_user/base_delete.py b/openslides_backend/action/actions/meeting_user/base_delete.py new file mode 100644 index 0000000000..171deb21c0 --- /dev/null +++ b/openslides_backend/action/actions/meeting_user/base_delete.py @@ -0,0 +1,25 @@ +from openslides_backend.shared.patterns import fqid_from_collection_and_id +from openslides_backend.shared.typing import HistoryInformation + +from ....models.models import MeetingUser +from ...generics.delete import DeleteAction +from ...util.default_schema import DefaultSchema + + +class MeetingUserBaseDelete(DeleteAction): + """ + Base action to delete a meeting user. + """ + + model = MeetingUser() + 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"]): [ + "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/delete.py b/openslides_backend/action/actions/meeting_user/delete.py index da82a4a221..16da011da5 100644 --- a/openslides_backend/action/actions/meeting_user/delete.py +++ b/openslides_backend/action/actions/meeting_user/delete.py @@ -1,37 +1,21 @@ from typing import Any from openslides_backend.shared.patterns import fqid_from_collection_and_id -from openslides_backend.shared.typing import HistoryInformation -from ....models.models import MeetingUser -from ...generics.delete import DeleteAction from ...util.action_type import ActionType -from ...util.default_schema import DefaultSchema from ...util.register import register_action from ..user.conditional_speaker_cascade_mixin import ( ConditionalSpeakerCascadeMixinHelper, ) +from .base_delete import MeetingUserBaseDelete @register_action("meeting_user.delete", action_type=ActionType.BACKEND_INTERNAL) -class MeetingUserDelete(ConditionalSpeakerCascadeMixinHelper, DeleteAction): +class MeetingUserDelete(ConditionalSpeakerCascadeMixinHelper, MeetingUserBaseDelete): """ Action to delete a meeting user. """ - model = MeetingUser() - 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"]): [ - "Participant removed from meeting {}", - fqid_from_collection_and_id("meeting", user["meeting_id"]), - ] - for user in users - } - def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: meeting_user = self.datastore.get( fqid_from_collection_and_id("meeting_user", instance["id"]), diff --git a/openslides_backend/action/actions/meeting_user/history_mixin.py b/openslides_backend/action/actions/meeting_user/history_mixin.py index 6b8fc68e28..8caa71cc50 100644 --- a/openslides_backend/action/actions/meeting_user/history_mixin.py +++ b/openslides_backend/action/actions/meeting_user/history_mixin.py @@ -255,7 +255,7 @@ def handle_group_updates( group_information: list[str] = [] if added and removed: group_information.append("Groups changed") - else: + elif added or removed: if added: group_information.append("Participant added to") else: @@ -272,7 +272,8 @@ def handle_group_updates( group_information.append( fqid_from_collection_and_id("meeting", meeting_id) ) - instance_information.append(tuple(group_information)) + if group_information: + instance_information.append(tuple(group_information)) def handle_delegations( self, diff --git a/openslides_backend/action/actions/motion/create.py b/openslides_backend/action/actions/motion/create.py index ce1664674a..88f01fbe8a 100644 --- a/openslides_backend/action/actions/motion/create.py +++ b/openslides_backend/action/actions/motion/create.py @@ -53,7 +53,7 @@ class MotionCreate( required_properties=["meeting_id", "title"], additional_optional_fields={ "workflow_id": optional_id_schema, - "submitter_ids": id_list_schema, + "submitter_meeting_user_ids": id_list_schema, "amendment_paragraphs": number_string_json_schema, "attachment_mediafile_ids": id_list_schema, "supporter_meeting_user_ids": id_list_schema, @@ -148,6 +148,41 @@ def check_permissions(self, instance: dict[str, Any]) -> None: if not has_perm(self.datastore, self.user_id, perm, instance["meeting_id"]): raise MissingPermission(perm) + extra_submitter_perms: list[Permission] = [ + Permissions.User.CAN_SEE, + Permissions.Motion.CAN_MANAGE_METADATA, + ] + if ( + (submitter_mu_ids := instance.get("submitter_meeting_user_ids")) + and ( + len(submitter_mu_ids) > 1 + or ( + submitter_mu_ids[0] + != ( + self.get_meeting_user( + instance["meeting_id"], self.user_id, ["id"] + ) + or {} + ).get("id") + ) + ) + and len( + missing_perms := { + perm: instance["meeting_id"] + for perm in extra_submitter_perms + if not has_perm( + self.datastore, + self.user_id, + perm, + instance["meeting_id"], + ) + } + ) + ): + raise MissingPermission( + {key: val for key, val in missing_perms.items()}, use_and=True + ) + # Whitelist the fields depending on the user's permissions. Each field can require multiple conjunctive permissions. can_manage_whitelist = set() forbidden_fields = defaultdict(set) @@ -156,9 +191,7 @@ def check_permissions(self, instance: dict[str, Any]) -> None: Permissions.Mediafile.CAN_SEE: ["attachment_mediafile_ids"], Permissions.Motion.CAN_MANAGE_METADATA: [ "additional_submitter", - "submitter_ids", ], - Permissions.User.CAN_SEE: ["submitter_ids"], } for perm, fields in permission_to_fields.items(): has_permission = has_perm( @@ -191,6 +224,7 @@ def check_permissions(self, instance: dict[str, Any]) -> None: "workflow_id", "id", "meeting_id", + "submitter_meeting_user_ids", ] ) if instance.get("lead_motion_id"): diff --git a/openslides_backend/action/actions/motion/create_base.py b/openslides_backend/action/actions/motion/create_base.py index cc6d86c430..c33971e23a 100644 --- a/openslides_backend/action/actions/motion/create_base.py +++ b/openslides_backend/action/actions/motion/create_base.py @@ -51,15 +51,10 @@ def set_state_from_workflow( ) def create_submitters(self, instance: dict[str, Any]) -> None: - submitter_ids = instance.pop("submitter_ids", []) - if not submitter_ids and not instance.get("additional_submitter"): - submitter_ids = [self.user_id] + submitter_ids = instance.pop("submitter_meeting_user_ids", []) self.apply_instance(instance) weight = 1 - for user_id in submitter_ids: - meeting_user_id = self.create_or_get_meeting_user( - instance["meeting_id"], user_id - ) + for meeting_user_id in submitter_ids: data = { "motion_id": instance["id"], "meeting_user_id": meeting_user_id, diff --git a/openslides_backend/action/actions/personal_note/create.py b/openslides_backend/action/actions/personal_note/create.py index 15c268e4dc..d3f45764d8 100644 --- a/openslides_backend/action/actions/personal_note/create.py +++ b/openslides_backend/action/actions/personal_note/create.py @@ -9,7 +9,6 @@ ) from ...util.default_schema import DefaultSchema from ...util.register import register_action -from ..meeting_user.create import MeetingUserCreate from .mixins import PermissionMixin @@ -59,20 +58,8 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: filtered_meeting_user = self.datastore.filter( "meeting_user", filter_, ["id", "personal_note_ids"] ) - if filtered_meeting_user: - meeting_user = list(filtered_meeting_user.values())[0] - instance["meeting_user_id"] = meeting_user["id"] - else: - action_results = self.execute_other_action( - MeetingUserCreate, - [ - { - "user_id": self.user_id, - "meeting_id": instance["meeting_id"], - } - ], - ) - instance["meeting_user_id"] = action_results[0]["id"] # type: ignore + meeting_user = list(filtered_meeting_user.values())[0] + instance["meeting_user_id"] = meeting_user["id"] if not (instance.get("star") or instance.get("note")): raise ActionException("Can't create personal note without star or note.") diff --git a/openslides_backend/action/actions/user/update.py b/openslides_backend/action/actions/user/update.py index be54f98346..0350041301 100644 --- a/openslides_backend/action/actions/user/update.py +++ b/openslides_backend/action/actions/user/update.py @@ -15,9 +15,11 @@ from ....shared.patterns import fqid_from_collection_and_id from ....shared.schema import optional_id_schema from ...generics.update import UpdateAction +from ...mixins.meeting_user_helper import get_meeting_user_filter from ...mixins.send_email_mixin import EmailCheckMixin from ...util.default_schema import DefaultSchema from ...util.register import register_action +from ..meeting_user.base_delete import MeetingUserBaseDelete from ..meeting_user.mixin import CheckLockOutPermissionMixin from .conditional_speaker_cascade_mixin import ConditionalSpeakerCascadeMixin from .user_mixins import ( @@ -29,6 +31,14 @@ ) +class MeetingUserDeleteInternal(MeetingUserBaseDelete): + """ + Action to delete a meeting user. + """ + + name = "meeting_user.delete_internal_helper" + + @register_action("user.update") class UserUpdate( UserMixin, @@ -116,7 +126,17 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: self.check_locking_status( instance.get("meeting_id"), instance, instance["id"], None ) + 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", + get_meeting_user_filter(removed_meeting_id, instance["id"]), + [], + ) + self.execute_other_action( + MeetingUserDeleteInternal, [{"id": id_} for id_ in meeting_users] + ) home_committee_id = instance.get("home_committee_id") user = self.datastore.get( fqid_from_collection_and_id("user", instance["id"]), diff --git a/openslides_backend/migrations/migrations/0074_remove_groupless_users.py b/openslides_backend/migrations/migrations/0074_remove_groupless_users.py new file mode 100644 index 0000000000..762fd6336f --- /dev/null +++ b/openslides_backend/migrations/migrations/0074_remove_groupless_users.py @@ -0,0 +1,347 @@ +from collections import defaultdict +from enum import Enum, auto +from time import time +from typing import Any, Literal, TypedDict + +from datastore.migrations import BaseModelMigration +from datastore.reader.core import GetManyRequestPart +from datastore.writer.core.write_request import ( + BaseRequestEvent, + RequestDeleteEvent, + RequestUpdateEvent, +) + +from ...shared.filters import And, FilterOperator, Or +from ...shared.patterns import collection_and_id_from_fqid + + +class CountdownCommand(Enum): + START = auto() + STOP = auto() + RESET = auto() + RESTART = auto() + + +class MigrationDataField(TypedDict): + to_collection: str + to_field: str + on_delete: Literal["cascade", "special"] | None + + +SPEAKER_EXTRA_FIELDS: list[str] = [] + +COLLECTION_TO_MIGRATION_FIELDS: dict[str, dict[str, MigrationDataField]] = { + "meeting_user": { + "user_id": { + "to_collection": "user", + "to_field": "meeting_user_ids", + "on_delete": None, + }, + "meeting_id": { + "to_collection": "meeting", + "to_field": "meeting_user_ids", + "on_delete": None, + }, + "personal_note_ids": { + "to_collection": "personal_note", + "to_field": "meeting_user_id", + "on_delete": "cascade", + }, + "speaker_ids": { + "to_collection": "speaker", + "to_field": "meeting_user_id", + "on_delete": None, + }, + "motion_supporter_ids": { + "to_collection": "motion_supporter", + "to_field": "meeting_user_id", + "on_delete": None, + }, + "motion_editor_ids": { + "to_collection": "motion_editor", + "to_field": "meeting_user_id", + "on_delete": None, + }, + "motion_working_group_speaker_ids": { + "to_collection": "motion_working_group_speaker", + "to_field": "meeting_user_id", + "on_delete": None, + }, + "motion_submitter_ids": { + "to_collection": "motion_submitter", + "to_field": "meeting_user_id", + "on_delete": None, + }, + "assignment_candidate_ids": { + "to_collection": "assignment_candidate", + "to_field": "meeting_user_id", + "on_delete": None, + }, + "vote_delegated_to_id": { + "to_collection": "meeting_user", + "to_field": "vote_delegations_from_ids", + "on_delete": None, + }, + "vote_delegations_from_ids": { + "to_collection": "meeting_user", + "to_field": "vote_delegated_to_id", + "on_delete": None, + }, + "chat_message_ids": { + "to_collection": "chat_message", + "to_field": "meeting_user_id", + "on_delete": None, + }, + "structure_level_ids": { + "to_collection": "structure_level", + "to_field": "meeting_user_ids", + "on_delete": None, + }, + }, + "speaker": { + "list_of_speakers_id": { + "to_collection": "list_of_speakers", + "to_field": "speaker_ids", + "on_delete": None, + }, + "structure_level_list_of_speakers_id": { + "to_collection": "structure_level_list_of_speakers", + "to_field": "speaker_ids", + "on_delete": None, + }, + "point_of_order_category_id": { + "to_collection": "point_of_order_category", + "to_field": "speaker_ids", + "on_delete": None, + }, + "meeting_id": { + "to_collection": "meeting", + "to_field": "speaker_ids", + "on_delete": None, + }, + }, + "personal_note": { + "content_object_id": { + "to_collection": "motion", + "to_field": "personal_note_ids", + "on_delete": "special", # bc generic but only points to motion + }, + "meeting_id": { + "to_collection": "meeting", + "to_field": "personal_note_ids", + "on_delete": None, + }, + }, +} + + +def is_list_field(field: str) -> bool: + return field.endswith("_ids") + + +class Migration(BaseModelMigration): + """ + This migration removes meeting_users without groups + """ + + target_migration_index = 75 + + def migrate_models(self) -> list[BaseRequestEvent] | None: + self.end_time = round(time()) + filter_ = And( + Or( + FilterOperator("group_ids", "=", None), + FilterOperator("group_ids", "=", "[]"), + ), + FilterOperator("meta_deleted", "!=", True), + ) + musers_to_delete = self.reader.filter( + "meeting_user", + filter_, + list(COLLECTION_TO_MIGRATION_FIELDS["meeting_user"]), + ) + speaker_ids_to_delete = self.calculate_speakers_to_delete(musers_to_delete) + self.collection_to_model_ids_to_delete: dict[str, set[int]] = defaultdict(set) + self.collection_to_model_ids_to_delete["speaker"] = set(speaker_ids_to_delete) + + self.fqids_to_delete: set[str] = { + f"meeting_user/{id_}" for id_ in musers_to_delete + } + self.fqids_to_delete.update({f"speaker/{id_}" for id_ in speaker_ids_to_delete}) + + self.fqid_to_list_removal: dict[str, dict[str, list[int]]] = defaultdict( + lambda: defaultdict(list) + ) + self.fqid_to_empty_fields: dict[str, set[str]] = defaultdict(set) + self.migrate_collection("meeting_user", musers_to_delete) + while delete_collections := list(self.collection_to_model_ids_to_delete.keys()): + delete_collection = delete_collections[0] + delete_collection_ids = self.collection_to_model_ids_to_delete.pop( + delete_collection, None + ) + if delete_collection_ids: + data_to_delete = self.reader.get_many( + [ + GetManyRequestPart( + delete_collection, + list(delete_collection_ids), + list(COLLECTION_TO_MIGRATION_FIELDS[delete_collection]), + ) + ] + ).get(delete_collection, {}) + self.migrate_collection(delete_collection, data_to_delete) + events: list[BaseRequestEvent] = [ + ( + RequestDeleteEvent(fqid) + if fqid in self.fqids_to_delete + else RequestUpdateEvent( + fqid, + fields={ + field: None for field in self.fqid_to_empty_fields.get(fqid, []) + }, + list_fields=( + { + "remove": { + field: [val for val in lis] + for field, lis in list_data.items() + } + } + if (list_data := self.fqid_to_list_removal.get(fqid, {})) + else {} + ), + ) + ) + for fqid in self.fqids_to_delete.union( + self.fqid_to_empty_fields, self.fqid_to_list_removal + ) + ] + return events + + def migrate_collection( + self, collection: str, models_to_delete: dict[int, dict[str, Any]] + ) -> None: + self.load_data(collection, models_to_delete) + for id_, model in models_to_delete.items(): + for field, value in model.items(): + if value and ( + field_data := COLLECTION_TO_MIGRATION_FIELDS[collection].get(field) + ): + if is_list_field(field): + for val_id in value: + self.handle_id(collection, id_, val_id, field, field_data) + else: + self.handle_id(collection, id_, value, field, field_data) + + def load_data( + self, collection: str, models_to_delete: dict[int, dict[str, Any]] + ) -> None: + fields = COLLECTION_TO_MIGRATION_FIELDS[collection] + collection_to_target_model_ids: dict[str, set[int]] = defaultdict(set) + for field, field_data in fields.items(): + collection_to_target_model_ids[field_data["to_collection"]].update( + [ + id_ if isinstance(id_, int) else id_.split("/")[1] + for mod in models_to_delete.values() + for id_ in (mod.get(field) or []) + ] + if is_list_field(field) + else [ + id_ if isinstance(id_, int) else id_.split("/")[1] + for mod in models_to_delete.values() + if (id_ := mod.get(field)) + ] + ) + self.existing_target_models = self.reader.get_many( + [ + GetManyRequestPart(coll, list(ids), ["id"]) + for coll, ids in collection_to_target_model_ids.items() + ] + ) + + def exists(self, fqid: str) -> bool: + collection, id_ = collection_and_id_from_fqid(fqid) + return id_ in self.existing_target_models.get(collection, {}) + + def handle_id( + self, + base_collection: str, + base_id: int, + value_id: int | str, + field: str, + field_data: MigrationDataField, + ) -> None: + target_fqid = f"{field_data['to_collection']}/{value_id}" + if target_fqid in self.fqids_to_delete: + return + match field_data["on_delete"]: + case "special": + if base_collection == "personal_note" and field == "content_object_id": + # back relation is always list-field personal_note_ids + assert isinstance(value_id, str) + target_fqid = value_id + if target_fqid in self.fqids_to_delete or not self.exists( + target_fqid + ): + return + self.fqid_to_list_removal[target_fqid]["personal_note_ids"].append( + base_id + ) + else: + raise Exception( + f"Bad migration: Handling of {base_collection}/{field} not defined." + ) + case "cascade": + if not self.exists(target_fqid): + return + assert isinstance(value_id, int) + self.collection_to_model_ids_to_delete[field_data["to_collection"]].add( + value_id + ) + self.fqids_to_delete.add(f"{field_data['to_collection']}/{value_id}") + case _: + if not self.exists(target_fqid): + return + if is_list_field(field_data["to_field"]): + self.fqid_to_list_removal[target_fqid][ + field_data["to_field"] + ].append(base_id) + else: + self.fqid_to_empty_fields[target_fqid].add(field_data["to_field"]) + + def calculate_speakers_to_delete( + self, musers_to_delete: dict[int, dict[str, Any]] + ) -> list[int]: + """ + Returns the list of speaker_ids that should be deleted + """ + speaker_ids = [ + speaker_id + for muser in musers_to_delete.values() + for speaker_id in (muser.get("speaker_ids") or []) + ] + speakers = self.reader.get_many( + [ + GetManyRequestPart( + "speaker", + speaker_ids, + [ + "meeting_id", + "list_of_speakers_id", + "structure_level_list_of_speakers_id", + "speech_state", + "begin_time", + "end_time", + "pause_time", + "point_of_order", + "unpause_time", + "id", + ], + ) + ] + ).get("speaker", {}) + delete_speaker_ids = [ + id_ + for id_, speaker in speakers.items() + if speaker.get("begin_time") is None + ] + return delete_speaker_ids diff --git a/openslides_backend/models/models.py b/openslides_backend/models/models.py index cf209a4569..d91f333796 100644 --- a/openslides_backend/models/models.py +++ b/openslides_backend/models/models.py @@ -198,7 +198,7 @@ class MeetingUser(Model): to={"chat_message": "meeting_user_id"}, equal_fields="meeting_id" ) group_ids = fields.RelationListField( - to={"group": "meeting_user_ids"}, equal_fields="meeting_id" + to={"group": "meeting_user_ids"}, required=True, equal_fields="meeting_id" ) structure_level_ids = fields.RelationListField( to={"structure_level": "meeting_user_ids"}, equal_fields="meeting_id" diff --git a/openslides_backend/shared/exceptions.py b/openslides_backend/shared/exceptions.py index 94ea1811d6..7b4615f66c 100644 --- a/openslides_backend/shared/exceptions.py +++ b/openslides_backend/shared/exceptions.py @@ -132,6 +132,7 @@ class MissingPermission(PermissionDenied): def __init__( self, permissions: AnyPermission | dict[AnyPermission, int | set[int]], + use_and: bool = False, ) -> None: if isinstance(permissions, dict): to_remove = [] @@ -141,7 +142,7 @@ def __init__( for permission in to_remove: del permissions[permission] self.message = "Missing permission" + self._plural_s(permissions) + ": " - self.message += " or ".join( + self.message += (" and " if use_and else " or ").join( f"{permission.get_verbose_type()} {permission} in {permission.get_base_model()}{self._plural_s(id_or_ids)} {id_or_ids}" for permission, id_or_ids in permissions.items() ) diff --git a/tests/system/action/chat_message/test_create.py b/tests/system/action/chat_message/test_create.py index ff96d82c89..b0b4680312 100644 --- a/tests/system/action/chat_message/test_create.py +++ b/tests/system/action/chat_message/test_create.py @@ -51,6 +51,23 @@ def test_create_correct_as_superadmin(self) -> None: assert model.get("chat_group_id") == 2 self.assert_model_exists("chat_group/2", {"chat_message_ids": [1]}) + def test_create_correct_as_superadmin_not_in_meeting(self) -> None: + self.create_meeting() + self.set_models( + { + "chat_group/2": {"meeting_id": 1, "write_group_ids": [3]}, + "group/3": {"meeting_id": 1}, + } + ) + response = self.request( + "chat_message.create", {"chat_group_id": 2, "content": "test"} + ) + self.assert_status_code(response, 400) + assert ( + "Cannot create chat message: You are not a participant of the meeting." + in response.json["message"] + ) + def test_create_correct_with_right_can_manage(self) -> None: self.create_meeting() self.set_models( diff --git a/tests/system/action/meeting/test_clone.py b/tests/system/action/meeting/test_clone.py index e353e0225b..b7a8cf3cb3 100644 --- a/tests/system/action/meeting/test_clone.py +++ b/tests/system/action/meeting/test_clone.py @@ -2178,7 +2178,11 @@ def test_create_clone_without_admin(self) -> None: ] ) self.set_models( - {"meeting_user/1": {"group_ids": []}, "group/2": {"meeting_user_ids": []}} + { + "meeting_user/1": {"group_ids": [1]}, + "group/1": {"meeting_user_ids": [1, 2, 3]}, + "group/2": {"meeting_user_ids": []}, + } ) response = self.request("meeting.clone", {"meeting_id": 1}) self.assert_status_code(response, 400) @@ -2229,7 +2233,8 @@ def test_create_clone_without_admin_2(self) -> None: ) self.set_models( { - "meeting_user/1": {"group_ids": None}, + "meeting_user/1": {"group_ids": [1]}, + "group/1": {"meeting_user_ids": [1, 2, 3]}, "group/2": {"meeting_user_ids": None}, } ) @@ -2400,6 +2405,7 @@ def test_clone_with_created_motion_and_agenda_type(self) -> None: "agenda_create": False, "agenda_type": AgendaItem.INTERNAL_ITEM, "agenda_duration": 60, + "submitter_meeting_user_ids": [1], }, ) self.assert_status_code(response, 200) diff --git a/tests/system/action/meeting/test_delete.py b/tests/system/action/meeting/test_delete.py index 32cd9b30fd..62f3c4ff85 100644 --- a/tests/system/action/meeting/test_delete.py +++ b/tests/system/action/meeting/test_delete.py @@ -422,6 +422,7 @@ def test_delete_archived_meeting(self) -> None: "meeting/1": { "user_ids": [2], "is_active_in_organization_id": None, + "meeting_user_ids": [2], }, } ) diff --git a/tests/system/action/meeting/test_import.py b/tests/system/action/meeting/test_import.py index 06605b9e0c..bc2d022e03 100644 --- a/tests/system/action/meeting/test_import.py +++ b/tests/system/action/meeting/test_import.py @@ -2011,6 +2011,7 @@ def test_without_default_password(self) -> None: assert "last_login" not in user def test_merge_meeting_users_fields(self) -> None: + self.create_meeting() self.set_models( { "user/14": { @@ -2027,7 +2028,9 @@ def test_merge_meeting_users_fields(self) -> None: "personal_note_ids": [1], "motion_submitter_ids": [], "vote_delegated_to_id": 1, + "group_ids": [1], }, + "group/1": {"meeting_user_ids": [14]}, "personal_note/1": { "meeting_id": 1, "content_object_id": None, @@ -2089,6 +2092,7 @@ def test_merge_meeting_users_fields(self) -> None: "personal_note_ids": [1], "motion_submitter_ids": [], "vote_delegated_to_id": 13, + "group_ids": [2], }, "13": { "id": 13, @@ -2097,12 +2101,14 @@ def test_merge_meeting_users_fields(self) -> None: "personal_note_ids": [2], "motion_submitter_ids": [], "vote_delegations_from_ids": [12], + "group_ids": [2], }, }, } ) request_data["meeting"]["meeting"]["1"]["personal_note_ids"] = [1, 2] request_data["meeting"]["meeting"]["1"]["meeting_user_ids"] = [11, 12, 13] + request_data["meeting"]["group"]["2"]["meeting_user_ids"] = [12, 13] response = self.request("meeting.import", request_data) self.assert_status_code(response, 200) self.assert_model_exists( diff --git a/tests/system/action/meeting_user/test_set_data.py b/tests/system/action/meeting_user/test_set_data.py index 00dac7445c..fcc2a8dd95 100644 --- a/tests/system/action/meeting_user/test_set_data.py +++ b/tests/system/action/meeting_user/test_set_data.py @@ -92,10 +92,10 @@ def test_set_data_with_meeting_user_and_wrong_user_id(self) -> None: self.request("meeting_user.set_data", test_dict) def test_set_data_without_meeting_user(self) -> None: + self.create_meeting(10) self.set_models( { "meeting/10": { - "is_active_in_organization_id": 1, "meeting_user_ids": [], "structure_level_ids": [31], }, @@ -110,6 +110,7 @@ def test_set_data_without_meeting_user(self) -> None: "structure_level_ids": [31], "about_me": "A very long description.", "vote_weight": "1.500000", + "group_ids": [12], } response = self.request("meeting_user.set_data", test_dict) self.assert_status_code(response, 200) diff --git a/tests/system/action/meeting_user/test_set_data_delegation.py b/tests/system/action/meeting_user/test_set_data_delegation.py index e033273655..a5a8d1cc16 100644 --- a/tests/system/action/meeting_user/test_set_data_delegation.py +++ b/tests/system/action/meeting_user/test_set_data_delegation.py @@ -94,20 +94,12 @@ def test_delegated_to_error_self(self) -> None: "User 4 can't delegate the vote to himself.", response.json["message"] ) - def test_delegated_to_success_without_group(self) -> None: + def test_delegated_to_without_group(self) -> None: response = self.request_executor({"group_ids": [], "vote_delegated_to_id": 13}) - self.assert_status_code(response, 200) - self.assert_model_exists( - "meeting_user/14", - { - "vote_delegated_to_id": 13, - "group_ids": [], - "meeting_id": 222, - "user_id": 4, - }, - ) - self.assert_model_exists( - "meeting_user/13", {"vote_delegations_from_ids": [12, 14]} + self.assert_status_code(response, 400) + self.assertIn( + "Update of meeting_user/14: You try to set following required fields to an empty value: ['group_ids']", + response.json["message"], ) def test_delegated_to_error_group_do_not_match_meeting(self) -> None: diff --git a/tests/system/action/motion/test_create.py b/tests/system/action/motion/test_create.py index 3a75d9e8c4..6514d3d133 100644 --- a/tests/system/action/motion/test_create.py +++ b/tests/system/action/motion/test_create.py @@ -6,7 +6,6 @@ ) from openslides_backend.models.models import AgendaItem from openslides_backend.permissions.base_classes import Permission -from openslides_backend.permissions.management_levels import OrganizationManagementLevel from openslides_backend.permissions.permissions import Permissions from tests.system.action.base import BaseActionTestCase @@ -33,6 +32,7 @@ def add_workflow(self) -> None: ) def test_create_good_case_required_fields(self) -> None: + self.set_user_groups(1, [1]) self.add_workflow() response = self.request( "motion.create", @@ -51,16 +51,13 @@ def test_create_good_case_required_fields(self) -> None: assert motion.get("workflow_timestamp") is not None assert motion.get("workflow_timestamp") == motion.get("last_modified") assert motion.get("created") == motion.get("last_modified") - assert motion.get("submitter_ids") == [1] + assert not motion.get("submitter_ids") assert motion.get("state_id") == 34 assert "agenda_create" not in motion - submitter = self.get_model("motion_submitter/1") - assert submitter.get("meeting_user_id") == 1 - assert submitter.get("meeting_id") == 1 - assert submitter.get("motion_id") == 1 + self.assert_model_not_exists("motion_submitter/1") self.assert_model_exists( "meeting_user/1", - {"meeting_id": 1, "user_id": 1, "motion_submitter_ids": [1]}, + {"meeting_id": 1, "user_id": 1, "motion_submitter_ids": None}, ) agenda_item = self.get_model("agenda_item/1") self.assertEqual(agenda_item.get("meeting_id"), 1) @@ -151,7 +148,7 @@ def test_create_normal_and_additional_submitter(self) -> None: "text": "test", "reason": "test", "additional_submitter": "test", - "submitter_ids": [bob_id], + "submitter_meeting_user_ids": [1], }, ) self.assert_status_code(response, 200) @@ -239,7 +236,6 @@ def test_create_with_set_number(self) -> None: "set_workflow_timestamp": True, "set_number": True, }, - "user/1": {"meeting_ids": [222]}, } ) response = self.request( @@ -254,6 +250,7 @@ def test_create_with_set_number(self) -> None: motion = self.assert_model_exists("motion/1", {"state_id": 34, "number": "1"}) assert motion.get("workflow_timestamp") assert motion.get("created") + self.assert_model_not_exists("meeting_user/1") def test_create_workflow_id_from_meeting(self) -> None: response = self.request( @@ -325,7 +322,7 @@ def test_create_with_submitters(self) -> None: "title": "test_Xcdfgee", "meeting_id": 1, "text": "text", - "submitter_ids": [56, 57], + "submitter_meeting_user_ids": [13, 14], }, ) self.assert_status_code(response, 200) @@ -501,12 +498,12 @@ def setup_permission_test( self.set_models(additional_data) return user_id - def test_create_no_permission_submitter(self) -> None: + def test_create_no_permission_additional_submitter(self) -> None: """ Asserts that the requesting user needs at least Motion.CAN_CREATE and - Motion.CAN_MANAGE_METADATA when sending submitter_ids and additional_submitter. - Also additionally for submitter_ids User.CAN_SEE. + Motion.CAN_MANAGE_METADATA when sending additional_submitter. """ + self.set_user_groups(1, [3]) user_id = self.setup_permission_test([Permissions.Motion.CAN_CREATE]) response = self.request( "motion.create", @@ -516,26 +513,55 @@ def test_create_no_permission_submitter(self) -> None: "text": "test", "reason": "test", "additional_submitter": "test", - "submitter_ids": [1, user_id], }, ) self.assert_status_code(response, 403) assert ( - "You are not allowed to perform action motion.create. Forbidden fields: additional_submitter with possibly needed permission(s): motion.can_manage, motion.can_manage_metadata, submitter_ids with possibly needed permission(s): motion.can_manage, motion.can_manage_metadata, user.can_see" + "You are not allowed to perform action motion.create. Forbidden fields: additional_submitter with possibly needed permission(s): motion.can_manage, motion.can_manage_metadata" + == response.json["message"] + ) + self.assert_model_not_exists("motion/1") + self.assert_model_not_exists("motion_submitter/1") + self.assert_model_exists("meeting_user/1", {"meeting_id": 1, "user_id": 1}) + self.assert_model_exists( + "meeting_user/2", {"meeting_id": 1, "user_id": user_id} + ) + + def test_create_no_permission_submitter(self) -> None: + """ + Asserts that the requesting user needs at least Motion.CAN_CREATE, + Motion.CAN_MANAGE_METADATA and User.CAN_SEE when sending submitter_meeting_user_ids aside from himself. + """ + self.set_user_groups(1, [3]) + user_id = self.setup_permission_test([Permissions.Motion.CAN_CREATE]) + response = self.request( + "motion.create", + { + "title": "test_Xcdfgee", + "meeting_id": 1, + "text": "test", + "reason": "test", + "submitter_meeting_user_ids": [1, 2], + }, + ) + self.assert_status_code(response, 403) + assert ( + "You are not allowed to perform action motion.create. Missing permissions: Permission user.can_see in meeting 1 and Permission motion.can_manage_metadata in meeting 1" == response.json["message"] ) self.assert_model_not_exists("motion/1") self.assert_model_not_exists("motion_submitter/1") + self.assert_model_exists("meeting_user/1", {"meeting_id": 1, "user_id": 1}) self.assert_model_exists( - "meeting_user/1", {"meeting_id": 1, "user_id": user_id} + "meeting_user/2", {"meeting_id": 1, "user_id": user_id} ) def test_create_no_user_can_see_submitter(self) -> None: """ Asserts that the requesting user needs at least Motion.CAN_CREATE and - Motion.CAN_MANAGE_METADATA, User.CAN_SEE when sending submitter_ids. - Also asserts that the error message contains Motion.CAN_MANAGE as possible permission. + Motion.CAN_MANAGE_METADATA, User.CAN_SEE when sending submitter_meeting_user_ids. """ + self.set_user_groups(1, [3]) user_id = self.setup_permission_test( [Permissions.Motion.CAN_CREATE, Permissions.Motion.CAN_MANAGE_METADATA] ) @@ -546,26 +572,89 @@ def test_create_no_user_can_see_submitter(self) -> None: "meeting_id": 1, "text": "test", "reason": "test", - "submitter_ids": [1, user_id], + "submitter_meeting_user_ids": [1, 2], }, ) self.assert_status_code(response, 403) assert ( - "You are not allowed to perform action motion.create. Forbidden fields: submitter_ids with possibly needed permission(s): motion.can_manage, user.can_see" + "You are not allowed to perform action motion.create. Missing permission: Permission user.can_see in meeting 1" == response.json["message"] ) self.assert_model_not_exists("motion/1") self.assert_model_not_exists("motion_submitter/1") + self.assert_model_exists("meeting_user/1", {"meeting_id": 1, "user_id": 1}) + self.assert_model_exists( + "meeting_user/2", {"meeting_id": 1, "user_id": user_id} + ) + + def test_create_no_motion_can_manage_metadata_submitter(self) -> None: + """ + Asserts that the requesting user needs at least Motion.CAN_CREATE, + Motion.CAN_MANAGE_METADATA and User.CAN_SEE when sending submitter_meeting_user_ids. + """ + self.set_user_groups(1, [3]) + user_id = self.setup_permission_test( + [Permissions.Motion.CAN_CREATE, Permissions.User.CAN_SEE] + ) + response = self.request( + "motion.create", + { + "title": "test_Xcdfgee", + "meeting_id": 1, + "text": "test", + "reason": "test", + "submitter_meeting_user_ids": [1, 2], + }, + ) + self.assert_status_code(response, 403) + assert ( + "You are not allowed to perform action motion.create. Missing permission: Permission motion.can_manage_metadata in meeting 1" + == response.json["message"] + ) + self.assert_model_not_exists("motion/1") + self.assert_model_not_exists("motion_submitter/1") + self.assert_model_exists("meeting_user/1", {"meeting_id": 1, "user_id": 1}) + self.assert_model_exists( + "meeting_user/2", {"meeting_id": 1, "user_id": user_id} + ) + + def test_create_no_user_can_see_submitter_self(self) -> None: + """ + Asserts that the requesting user needs at least Motion.CAN_CREATE and + Motion.CAN_MANAGE_METADATA, but not User.CAN_SEE when setting himself as submitter. + """ + self.set_user_groups(1, [3]) + user_id = self.setup_permission_test( + [Permissions.Motion.CAN_CREATE, Permissions.Motion.CAN_MANAGE_METADATA] + ) + response = self.request( + "motion.create", + { + "title": "test_Xcdfgee", + "meeting_id": 1, + "text": "test", + "reason": "test", + "submitter_meeting_user_ids": [user_id], + }, + ) + self.assert_status_code(response, 200) + self.assert_model_exists("motion/1") self.assert_model_exists( - "meeting_user/1", {"meeting_id": 1, "user_id": user_id} + "motion_submitter/1", {"motion_id": 1, "meeting_user_id": 2, "weight": 1} + ) + self.assert_model_exists("meeting_user/1", {"meeting_id": 1, "user_id": 1}) + self.assert_model_exists( + "meeting_user/2", + {"meeting_id": 1, "user_id": user_id, "motion_submitter_ids": [1]}, ) def test_create_no_permission_additional_submitter_enabled(self) -> None: """ Asserts that the requesting user needs at least Motion.CAN_CREATE and - Motion.CAN_MANAGE_METADATA when sending submitter_ids and additional_submitter. - Also additionally for submitter_ids User.CAN_SEE. + Motion.CAN_MANAGE_METADATA when sending submitter_meeting_user_ids and additional_submitter. + Also additionally for submitter_meeting_user_ids User.CAN_SEE. """ + self.set_user_groups(1, [3]) user_id = self.setup_permission_test([Permissions.Motion.CAN_CREATE]) self.update_model( "meeting/1", {"motions_create_enable_additional_submitter_text": True} @@ -578,18 +667,19 @@ def test_create_no_permission_additional_submitter_enabled(self) -> None: "text": "test", "reason": "test", "additional_submitter": "test", - "submitter_ids": [1, user_id], + "submitter_meeting_user_ids": [1, 2], }, ) self.assert_status_code(response, 403) assert ( - "You are not allowed to perform action motion.create. Forbidden fields: additional_submitter with possibly needed permission(s): motion.can_manage, motion.can_manage_metadata, submitter_ids with possibly needed permission(s): motion.can_manage, motion.can_manage_metadata, user.can_see" + "You are not allowed to perform action motion.create. Missing permissions: Permission user.can_see in meeting 1 and Permission motion.can_manage_metadata in meeting 1" == response.json["message"] ) self.assert_model_not_exists("motion/1") self.assert_model_not_exists("motion_submitter/1") + self.assert_model_exists("meeting_user/1", {"meeting_id": 1, "user_id": 1}) self.assert_model_exists( - "meeting_user/1", {"meeting_id": 1, "user_id": user_id} + "meeting_user/2", {"meeting_id": 1, "user_id": user_id} ) def test_create_permission_agenda_allowed(self) -> None: @@ -910,69 +1000,3 @@ def test_create_with_irrelevant_delegator_setting(self) -> None: }, ) self.assert_status_code(response, 200) - - def base_assign_external_self_test(self, oml: OrganizationManagementLevel) -> None: - """also tests the history collection feature in case of the creation of multiple history entries with just one history position""" - bob_id = self.create_user("bob", organization_management_level=oml) - self.login(bob_id) - response = self.request_multi( - "motion.create", - [ - { - "title": "Submitter is me", - "meeting_id": 1, - "text": "test", - }, - { - "title": "Submitter is me 2", - "meeting_id": 1, - "text": "test 2", - "submitter_ids": [bob_id], - }, - ], - ) - self.assert_status_code(response, 200) - self.assert_model_exists( - "meeting_user/1", {"user_id": bob_id, "motion_submitter_ids": [1, 2]} - ) - self.assert_model_exists("motion_submitter/1", {"motion_id": 1}) - self.assert_model_exists("motion_submitter/2", {"motion_id": 2}) - self.assert_model_exists( - "history_position/1", - {"original_user_id": bob_id, "user_id": bob_id, "entry_ids": [1, 2, 3]}, - ) - self.assert_model_exists( - "history_entry/1", - { - "entries": ["Participant added to meeting {}.", "meeting/1"], - "original_model_id": f"user/{bob_id}", - "model_id": f"user/{bob_id}", - "position_id": 1, - }, - ) - self.assert_model_exists( - "history_entry/2", - { - "entries": ["Motion created"], - "original_model_id": "motion/1", - "model_id": "motion/1", - "position_id": 1, - }, - ) - self.assert_model_exists( - "history_entry/3", - { - "entries": ["Motion created"], - "original_model_id": "motion/2", - "model_id": "motion/2", - "position_id": 1, - }, - ) - - def test_create_assign_self_with_external_superadmin(self) -> None: - self.base_assign_external_self_test(OrganizationManagementLevel.SUPERADMIN) - - def test_create_assign_self_with_external_orga_admin(self) -> None: - self.base_assign_external_self_test( - OrganizationManagementLevel.CAN_MANAGE_ORGANIZATION - ) diff --git a/tests/system/action/motion/test_create_sequential_number.py b/tests/system/action/motion/test_create_sequential_number.py index ee847ac243..fdaf6eb3ac 100644 --- a/tests/system/action/motion/test_create_sequential_number.py +++ b/tests/system/action/motion/test_create_sequential_number.py @@ -27,12 +27,8 @@ def create_workflow(self, workflow_id: int = 12, meeting_id: int = 222) -> None: ) def test_create_sequential_numbers(self) -> None: - self.set_models( - { - "meeting/222": {"is_active_in_organization_id": 1, "committee_id": 1}, - "user/1": {"meeting_ids": [222]}, - } - ) + self.create_meeting(222) + self.set_user_groups(1, [223]) self.create_workflow() response = self.request( @@ -63,21 +59,9 @@ def test_create_sequential_numbers(self) -> None: self.assertEqual(model.get("sequential_number"), 2) def test_create_sequential_numbers_2meetings(self) -> None: - self.set_models( - { - "meeting/222": { - "name": "meeting222", - "is_active_in_organization_id": 1, - "committee_id": 1, - }, - "meeting/223": { - "name": "meeting223", - "is_active_in_organization_id": 1, - "committee_id": 2, - }, - "user/1": {"meeting_ids": [222]}, - } - ) + self.create_meeting(222) + self.create_meeting(225) + self.set_user_groups(1, [223, 225]) self.create_workflow() response = self.request( @@ -93,12 +77,12 @@ def test_create_sequential_numbers_2meetings(self) -> None: model = self.get_model("motion/1") self.assertEqual(model.get("sequential_number"), 1) - self.create_workflow(workflow_id=13, meeting_id=223) + self.create_workflow(workflow_id=13, meeting_id=225) response = self.request( "motion.create", { "title": "motion_title", - "meeting_id": 223, + "meeting_id": 225, "workflow_id": 13, "text": "test", }, @@ -108,12 +92,8 @@ def test_create_sequential_numbers_2meetings(self) -> None: self.assertEqual(model.get("sequential_number"), 1) def test_create_sequential_numbers_deleted_motion(self) -> None: - self.set_models( - { - "meeting/222": {"is_active_in_organization_id": 1, "committee_id": 1}, - "user/1": {"meeting_ids": [222]}, - } - ) + self.create_meeting(222) + self.set_user_groups(1, [223]) self.create_workflow() response = self.request( @@ -162,12 +142,8 @@ def test_create_sequential_numbers_race_condition(self) -> None: ActionHandler.MAX_RETRY = 3 self.set_thread_watch_timeout(-2) pytest_thread_local.name = "MainThread_RC" - self.set_models( - { - "meeting/222": {"is_active_in_organization_id": 1, "committee_id": 1}, - "user/1": {"meeting_ids": [222]}, - } - ) + self.create_meeting(222) + self.set_user_groups(1, [223]) self.create_workflow(workflow_id=12, meeting_id=222) self.create_workflow(workflow_id=13, meeting_id=222) diff --git a/tests/system/action/motion/test_set_number_mixin.py b/tests/system/action/motion/test_set_number_mixin.py index 4acf7c2a32..dd0a4abada 100644 --- a/tests/system/action/motion/test_set_number_mixin.py +++ b/tests/system/action/motion/test_set_number_mixin.py @@ -4,6 +4,7 @@ class MotionSetNumberMixinTest(BaseActionTestCase): def test_create_set_number_return_because_number_preset(self) -> None: self.create_meeting(222) + self.set_user_groups(1, [223]) self.set_models( { "meeting/222": { @@ -40,6 +41,7 @@ def test_create_set_number_return_because_number_preset(self) -> None: def test_create_set_number_return_because_number_type_manually(self) -> None: self.create_meeting(222) + self.set_user_groups(1, [223]) self.set_models( { "meeting/222": { @@ -80,6 +82,7 @@ def test_create_set_number_return_because_number_type_manually(self) -> None: def test_create_set_number_good(self) -> None: self.create_meeting(222) + self.set_user_groups(1, [223]) self.set_models( { "user/1": {"meeting_ids": [222]}, @@ -113,6 +116,7 @@ def test_create_set_number_good(self) -> None: def test_create_set_number_min_digits(self) -> None: self.create_meeting(222) + self.set_user_groups(1, [223]) self.set_models( { "meeting/222": { @@ -150,6 +154,7 @@ def test_create_set_number_min_digits(self) -> None: def test_create_set_number_prefix_blank_lead_motion(self) -> None: self.create_meeting(222) + self.set_user_groups(1, [223]) self.set_models( { "meeting/222": { @@ -196,6 +201,7 @@ def test_create_set_number_prefix_blank_lead_motion(self) -> None: def test_create_set_number_prefix_blank_lead_motion_number_inc(self) -> None: self.create_meeting(222) + self.set_user_groups(1, [223]) self.set_models( { "meeting/222": { @@ -251,6 +257,7 @@ def test_create_set_number_prefix_blank_lead_motion_number_inc(self) -> None: def test_create_set_number_get_number_per_category(self) -> None: self.create_meeting(222) + self.set_user_groups(1, [223]) self.set_models( { "meeting/222": { @@ -300,6 +307,7 @@ def test_create_set_number_get_number_per_category(self) -> None: def test_create_set_number_unique_check_jump(self) -> None: self.create_meeting(222) + self.set_user_groups(1, [223]) self.set_models( { "meeting/222": { @@ -357,6 +365,7 @@ def test_create_set_number_unique_check_jump(self) -> None: def test_set_number_false(self) -> None: self.create_meeting(222) + self.set_user_groups(1, [223]) self.set_models( { "meeting/222": { @@ -395,6 +404,7 @@ def test_set_number_false(self) -> None: class SetNumberMixinSetStateTest(BaseActionTestCase): def test_set_state_correct_next_state(self) -> None: self.create_meeting(222) + self.set_user_groups(1, [223]) self.set_models( { "user/1": {"meeting_ids": [222]}, @@ -427,6 +437,7 @@ def test_set_state_correct_next_state(self) -> None: class SetNumberMixinManuallyTest(BaseActionTestCase): def _create_models_for_number_manually_tests(self) -> None: self.create_meeting(222) + self.set_user_groups(1, [223]) self.set_models( { "meeting/222": { @@ -524,6 +535,7 @@ def test_complex_example_manually_3(self) -> None: class SetNumberMixinSerialTest(BaseActionTestCase): def _create_models_for_number_prefix_test(self) -> None: self.create_meeting(222) + self.set_user_groups(1, [223]) self.set_models( { "meeting/222": { @@ -681,6 +693,7 @@ def test_complex_example_serially_numbered_3(self) -> None: class SetNumberMixinComplexExamplesPerCategoryTest(BaseActionTestCase): def _create_models_for_number_per_category_1(self) -> None: self.create_meeting(222) + self.set_user_groups(1, [223]) self.set_models( { "meeting/222": { @@ -844,6 +857,7 @@ def test_complex_example_per_category_1_2(self) -> None: def _create_models_for_number_per_category_2(self) -> None: self.create_meeting(222) + self.set_user_groups(1, [223]) self.set_models( { "meeting/222": { @@ -1105,6 +1119,7 @@ def test_complex_example_per_category_2_4(self) -> None: class SetNumberMixinFollowRecommandationTest(BaseActionTestCase): def test_set_number(self) -> None: self.create_meeting(222) + self.set_user_groups(1, [223]) self.set_models( { "user/1": {"meeting_ids": [222]}, diff --git a/tests/system/action/personal_note/test_create.py b/tests/system/action/personal_note/test_create.py index 0674672ee9..520f8b5d1e 100644 --- a/tests/system/action/personal_note/test_create.py +++ b/tests/system/action/personal_note/test_create.py @@ -126,3 +126,16 @@ def test_create_other_meeting_user(self) -> None: ) self.assert_status_code(response, 200) self.assert_model_exists("personal_note/1") + + def test_create_not_in_meeting(self) -> None: + self.create_meeting() + self.set_models( + { + "motion/23": {"meeting_id": 1}, + } + ) + response = self.request( + "personal_note.create", {"content_object_id": "motion/23", "star": True} + ) + self.assert_status_code(response, 403) + assert "User not associated with meeting." in response.json["message"] diff --git a/tests/system/action/test_action_worker.py b/tests/system/action/test_action_worker.py index 9b3d12f24f..e7e659f4c6 100644 --- a/tests/system/action/test_action_worker.py +++ b/tests/system/action/test_action_worker.py @@ -36,6 +36,7 @@ def setUp(self) -> None: def test_action_worker_ready_before_timeout_okay(self) -> None: """action thread used, but ended in time""" + self.set_user_groups(1, [223]) response = self.request( "motion.create", { @@ -70,6 +71,7 @@ def test_action_worker_not_ready_before_timeout_okay(self) -> None: """action thread used, main process ends before action_worker is ready, but the final result will be okay. """ + self.set_user_groups(1, [223]) self.set_thread_watch_timeout(0) count_motions: int = 2 response = self.request_multi( diff --git a/tests/system/action/user/test_create.py b/tests/system/action/user/test_create.py index 7f6ec46fbc..ccef6c3693 100644 --- a/tests/system/action/user/test_create.py +++ b/tests/system/action/user/test_create.py @@ -151,15 +151,14 @@ def test_create_some_more_fields(self) -> None: ) def test_create_comment(self) -> None: - self.set_models( - {"meeting/1": {"name": "test meeting 1", "is_active_in_organization_id": 1}} - ) + self.create_meeting() response = self.request( "user.create", { "username": "test_Xcdfgee", "comment": "blablabla", "meeting_id": 1, + "group_ids": [1], }, ) self.assert_status_code(response, 200) @@ -380,16 +379,13 @@ def test_member_number_none(self) -> None: self.assert_model_exists("user/2", {"member_number": None}) def test_user_create_with_empty_vote_delegation_from_ids(self) -> None: - self.set_models( - { - "meeting/1": {"is_active_in_organization_id": 1}, - } - ) + self.create_meeting() response = self.request( "user.create", { "username": "testname", "meeting_id": 1, + "group_ids": [3], "vote_delegations_from_ids": [], "organization_management_level": OrganizationManagementLevel.CAN_MANAGE_USERS, }, @@ -1474,6 +1470,7 @@ def assert_lock_out_user( "username": "test", "meeting_id": meeting_id, "locked_out": True, + "group_ids": [1], **other_payload_data, }, ) diff --git a/tests/system/action/user/test_delegation_history.py b/tests/system/action/user/test_delegation_history.py index 8cf32f2952..1b68d03be6 100644 --- a/tests/system/action/user/test_delegation_history.py +++ b/tests/system/action/user/test_delegation_history.py @@ -99,7 +99,7 @@ def test_update_receive_delegated_vote(self) -> None: self.assert_delegated_to(self.alice_id, self.bob_id) def test_create_delegate_vote(self) -> None: - self.make_request({"vote_delegated_to_id": self.bob_id - 1}) + self.make_request({"vote_delegated_to_id": self.bob_id - 1, "group_ids": [3]}) self.assert_delegated_to( self.next_user_id, self.bob_id, @@ -107,11 +107,16 @@ def test_create_delegate_vote(self) -> None: "Account created", "Participant added to meeting {}.", "meeting/1", + "Participant added to group {} in meeting {}.", + "group/3", + "meeting/1", ], ) def test_create_receive_delegated_vote(self) -> None: - self.make_request({"vote_delegations_from_ids": [self.alice_id - 1]}) + self.make_request( + {"vote_delegations_from_ids": [self.alice_id - 1], "group_ids": [3]} + ) self.assert_delegated_to( self.alice_id, self.next_user_id, @@ -119,6 +124,9 @@ def test_create_receive_delegated_vote(self) -> None: "Account created", "Participant added to meeting {}.", "meeting/1", + "Participant added to group {} in meeting {}.", + "group/3", + "meeting/1", ], ) @@ -136,13 +144,18 @@ def test_update_re_delegate_vote_reverse(self) -> None: def test_create_re_delegate_vote_reverse(self) -> None: self.setup_delegation() - self.make_request({"vote_delegations_from_ids": [self.alice_id - 1]}) + self.make_request( + {"vote_delegations_from_ids": [self.alice_id - 1], "group_ids": [3]} + ) self.assert_alice_redelegated_to( self.next_user_id, prepend=[ "Account created", "Participant added to meeting {}.", "meeting/1", + "Participant added to group {} in meeting {}.", + "group/3", + "meeting/1", ], ) @@ -327,7 +340,8 @@ def test_create_multiple_from_ids(self) -> None: eric_id, fredric_id, ] - ] + ], + "group_ids": [3], } ) self.assert_history_information( @@ -336,6 +350,9 @@ def test_create_multiple_from_ids(self) -> None: "Account created", "Participant added to meeting {}.", "meeting/1", + "Participant added to group {} in meeting {}.", + "group/3", + "meeting/1", "Proxy voting rights for {}, {}, {}, {}, {} received in meeting {}", *[ f"user/{id_}" @@ -362,7 +379,10 @@ def test_create_multiple_from_ids(self) -> None: def test_update_create_meeting_user_receiving_delegation(self) -> None: debra_id = self.create_user("debra") - self.make_request({"vote_delegations_from_ids": [self.alice_id - 1]}, debra_id) + self.make_request( + {"vote_delegations_from_ids": [self.alice_id - 1], "group_ids": [3]}, + debra_id, + ) self.assert_history_information( f"user/{self.alice_id}", ["Vote delegated to {} in meeting {}", f"user/{debra_id}", "meeting/1"], @@ -372,6 +392,9 @@ def test_update_create_meeting_user_receiving_delegation(self) -> None: [ "Participant added to meeting {}.", "meeting/1", + "Participant added to group {} in meeting {}.", + "group/3", + "meeting/1", "Proxy voting rights for {} received in meeting {}", f"user/{self.alice_id}", "meeting/1", @@ -380,7 +403,9 @@ def test_update_create_meeting_user_receiving_delegation(self) -> None: def test_update_create_meeting_user_with_delegation(self) -> None: debra_id = self.create_user("debra") - self.make_request({"vote_delegated_to_id": self.alice_id - 1}, debra_id) + self.make_request( + {"vote_delegated_to_id": self.alice_id - 1, "group_ids": [3]}, debra_id + ) self.assert_history_information( f"user/{self.alice_id}", [ @@ -394,6 +419,9 @@ def test_update_create_meeting_user_with_delegation(self) -> None: [ "Participant added to meeting {}.", "meeting/1", + "Participant added to group {} in meeting {}.", + "group/3", + "meeting/1", "Vote delegated to {} in meeting {}", f"user/{self.alice_id}", "meeting/1", diff --git a/tests/system/action/user/test_update.py b/tests/system/action/user/test_update.py index 3fabaa508c..493cc65995 100644 --- a/tests/system/action/user/test_update.py +++ b/tests/system/action/user/test_update.py @@ -375,27 +375,21 @@ def test_update_set_and_reset_vote_forwarded(self) -> None: ) def test_update_vote_weight(self) -> None: - self.set_models( - { - "user/111": {"username": "username_srtgb123"}, - "meeting/1": { - "name": "test_meeting_1", - "is_active_in_organization_id": 1, - }, - } - ) + self.create_meeting() + id_ = self.create_user("username_srtgb123") response = self.request( - "user.update", {"id": 111, "vote_weight": "2.000000", "meeting_id": 1} + "user.update", + {"id": id_, "vote_weight": "2.000000", "meeting_id": 1, "group_ids": [1]}, ) self.assert_status_code(response, 200) self.assert_model_exists( - "user/111", {"username": "username_srtgb123", "meeting_user_ids": [1]} + f"user/{id_}", {"username": "username_srtgb123", "meeting_user_ids": [1]} ) self.assert_model_exists( "meeting_user/1", { "meeting_id": 1, - "user_id": 111, + "user_id": id_, "vote_weight": "2.000000", }, ) @@ -505,20 +499,20 @@ 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( - "meeting_user/1111", {"group_ids": [], "meta_deleted": False} - ) + self.assert_model_deleted("meeting_user/1111", {"group_ids": []}) self.assert_history_information( "user/111", [ "Participant removed from group {} in meeting {}", "group/600", "meeting/60", + "Participant removed from meeting {}", + "meeting/60", "Personal data changed", "Committee management changed", ], @@ -638,9 +632,10 @@ 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_deleted("meeting_user/111") self.assert_model_exists("committee/1", {"user_ids": []}) self.assert_model_exists("committee/2", {"user_ids": [123]}) self.assert_model_exists("committee/3", {"user_ids": [123]}) @@ -2954,15 +2949,13 @@ def test_update_empty_cml_no_history(self) -> None: self.assert_history_information("user/111", None) def test_update_participant_data_with_existing_meetings(self) -> None: + self.create_meeting() + self.create_meeting(4) + bob_id = self.create_user("bob") + bob_muser_ids = self.set_user_groups(bob_id, [1]) self.set_models( { - "meeting/1": {"committee_id": 1, "is_active_in_organization_id": 1}, - "meeting/2": {"committee_id": 1, "is_active_in_organization_id": 1}, - "committee/1": {"meeting_ids": [1]}, - "user/222": {"meeting_user_ids": [42]}, - "meeting_user/42": { - "user_id": 222, - "meeting_id": 1, + f"meeting_user/{bob_muser_ids[0]}": { "vote_weight": "1.000000", }, } @@ -2970,33 +2963,35 @@ def test_update_participant_data_with_existing_meetings(self) -> None: response = self.request( "user.update", { - "id": 222, - "meeting_id": 2, + "id": bob_id, + "meeting_id": 4, "vote_weight": "1.500000", + "group_ids": [4], }, ) self.assert_status_code(response, 200) self.assert_history_information( - "user/222", + f"user/{bob_id}", [ "Participant added to meeting {}.", - "meeting/2", + "meeting/4", + "Participant added to group {} in meeting {}.", + "group/4", + "meeting/4", ], ) def test_update_participant_data_in_multiple_meetings_with_existing_meetings( self, ) -> None: + self.create_meeting() + self.create_meeting(4) + self.create_meeting(7) + bob_id = self.create_user("bob") + bob_muser_id = self.set_user_groups(bob_id, [1])[0] self.set_models( { - "meeting/1": {"committee_id": 1, "is_active_in_organization_id": 1}, - "meeting/2": {"committee_id": 1, "is_active_in_organization_id": 1}, - "meeting/3": {"committee_id": 1, "is_active_in_organization_id": 1}, - "committee/1": {"meeting_ids": [1]}, - "user/222": {"meeting_user_ids": [42]}, - "meeting_user/42": { - "user_id": 222, - "meeting_id": 1, + f"meeting_user/{bob_muser_id}": { "vote_weight": "1.000000", }, } @@ -3005,25 +3000,33 @@ def test_update_participant_data_in_multiple_meetings_with_existing_meetings( "user.update", [ { - "id": 222, - "meeting_id": 2, + "id": bob_id, + "meeting_id": 4, "vote_weight": "1.000000", + "group_ids": [4], }, { - "id": 222, - "meeting_id": 3, + "id": bob_id, + "meeting_id": 7, "vote_weight": "1.000000", + "group_ids": [7], }, ], ) self.assert_status_code(response, 200) self.assert_history_information( - "user/222", + f"user/{bob_id}", [ "Participant added to meeting {}.", - "meeting/2", + "meeting/4", + "Participant added to group {} in meeting {}.", + "group/4", + "meeting/4", "Participant added to meeting {}.", - "meeting/3", + "meeting/7", + "Participant added to group {} in meeting {}.", + "group/7", + "meeting/7", ], ) @@ -3108,7 +3111,7 @@ def test_group_removal_with_speaker(self) -> None: "user/1234", { "username": "username_abcdefgh123", - "meeting_user_ids": [4444, 5555], + "meeting_user_ids": [5555], "is_present_in_meeting_ids": [5], }, ) @@ -3124,16 +3127,16 @@ def test_group_removal_with_speaker(self) -> None: "present_user_ids": [1234], }, ) - self.assert_model_exists( + self.assert_model_deleted( "meeting_user/4444", - {"group_ids": [], "speaker_ids": [24], "meta_deleted": False}, + {"group_ids": [], "speaker_ids": [24]}, ) 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} @@ -3526,26 +3529,36 @@ def assert_lock_out_user( def test_update_locked_out_foreign_cml_allowed(self) -> None: self.assert_lock_out_user( - "account", 1, other_data={"committee_management_ids": [63]} + "account", + 1, + other_data={"committee_management_ids": [63], "group_ids": [1]}, ) def test_update_locked_out_user_child_cml_allowed(self) -> None: self.create_committee(60) self.create_committee(63, parent_id=60) self.assert_lock_out_user( - "account", 1, other_data={"committee_management_ids": [63]} + "account", + 1, + other_data={"committee_management_ids": [63], "group_ids": [1]}, ) def test_update_locked_out_user_home_committee_allowed(self) -> None: - self.assert_lock_out_user("account", 1, other_data={"home_committee_id": 60}) + self.assert_lock_out_user( + "account", 1, other_data={"home_committee_id": 60, "group_ids": [1]} + ) def test_update_locked_out_user_child_home_committee_allowed(self) -> None: self.create_committee(60) self.create_committee(63, parent_id=60) - self.assert_lock_out_user("account", 1, other_data={"home_committee_id": 63}) + self.assert_lock_out_user( + "account", 1, other_data={"home_committee_id": 63, "group_ids": [1]} + ) def test_update_locked_out_user_foreign_home_committee_allowed(self) -> None: - self.assert_lock_out_user("account", 1, other_data={"home_committee_id": 63}) + self.assert_lock_out_user( + "account", 1, other_data={"home_committee_id": 63, "group_ids": [1]} + ) def test_update_locked_out_superadmin_error(self) -> None: self.assert_lock_out_user( @@ -3703,12 +3716,16 @@ def test_update_meeting_admin_on_locked_out_user_error(self) -> None: def test_update_locked_out_remove_superadmin(self) -> None: self.assert_lock_out_user( - "superad", 1, other_data={"organization_management_level": None} + "superad", + 1, + other_data={"organization_management_level": None, "group_ids": [1]}, ) def test_update_locked_out_remove_cml(self) -> None: self.assert_lock_out_user( - "committeead60", 1, other_data={"committee_management_ids": None} + "committeead60", + 1, + other_data={"committee_management_ids": None, "group_ids": [1]}, ) def test_update_locked_out_remove_meeting_admin(self) -> None: @@ -4682,6 +4699,36 @@ def test_multi_delegation_doesnt_break_history(self) -> None: ) self.assert_status_code(response, 200) + def test_lock_out_then_remove_from_meeting_then_set_superadmin(self) -> None: + self.create_meeting(10) + bob_id = self.create_user("bob", group_ids=[10]) + response = self.request( + "user.update", + {"id": bob_id, "meeting_id": 10, "locked_out": True}, + ) + self.assert_status_code(response, 200) + self.assert_model_exists( + "meeting_user/1", {"locked_out": True, "user_id": bob_id, "meeting_id": 10} + ) + response = self.request( + "user.update", + {"id": bob_id, "meeting_id": 10, "group_ids": []}, + ) + self.assert_status_code(response, 200) + self.assert_model_deleted("meeting_user/1") + response = self.request( + "user.update", + { + "id": bob_id, + "organization_management_level": "superadmin", + }, + ) + self.assert_status_code(response, 200) + self.assert_model_exists( + f"user/{bob_id}", + {"username": "bob", "organization_management_level": "superadmin"}, + ) + class UserUpdateHomeCommitteePermissionTest(BaseActionTestCase): committeePerms: set[int] = set() diff --git a/tests/system/migrations/test_0074_remove_groupless_users.py b/tests/system/migrations/test_0074_remove_groupless_users.py new file mode 100644 index 0000000000..f10436898b --- /dev/null +++ b/tests/system/migrations/test_0074_remove_groupless_users.py @@ -0,0 +1,1386 @@ +from math import floor +from typing import Any + + +def get_muser( + id_: int, additional_data: dict[str, Any] = {} +) -> dict[int, dict[str, Any]]: + return { + id_: {"meeting_id": floor(id_ / 10), "user_id": id_ % 10, **additional_data} + } + + +def get_motion( + id_: int, additional_data: dict[str, Any] = {} +) -> dict[int, dict[str, Any]]: + return { + id_: { + "meeting_id": floor(id_ / 10), + "title": f"Motion {id_%10}", + "list_of_speakers_id": (id_ % 10) * 10 + floor(id_ / 10), + **additional_data, + } + } + + +def get_los( + id_: int, additional_data: dict[str, Any] = {} +) -> dict[int, dict[str, Any]]: + return { + id_: { + "meeting_id": id_ % 10, + "sequential_number": floor(id_ / 10), + "content_object_id": f"motion/{(id_%10)*10+floor(id_/10)}", + **additional_data, + } + } + + +def get_speaker( + id_: int, additional_data: dict[str, Any] = {} +) -> dict[int, dict[str, Any]]: + return { + id_: { + "meeting_id": floor(id_ / 100) % 10, + "list_of_speakers_id": floor(id_ / 100), + "meeting_user_id": id_ % 100, + **additional_data, + } + } + + +def get_sllos( + id_: int, additional_data: dict[str, Any] = {} +) -> dict[int, dict[str, Any]]: + return { + id_: { + "meeting_id": (floor(id_ / 10) % 10), + "structure_level_id": id_ % 10, + "list_of_speakers_id": floor(id_ / 10), + **additional_data, + } + } + + +MOTION_MEETING_USER_MODELS = [ + "motion_editor", + "motion_working_group_speaker", + "motion_submitter", + "motion_supporter", +] + + +def test_simple(write, finalize, assert_model): + test_data: dict[str, dict[str, Any]] = { + "user/1": {"meeting_ids": [1], "meeting_user_ids": [1]}, + "meeting/1": { + "user_ids": [1], + "meeting_user_ids": [1], + }, + "meeting_user/1": {"user_id": 1, "meeting_id": 1, "group_ids": []}, + } + + write( + *[ + {"type": "create", "fqid": fqid, "fields": data} + for fqid, data in test_data.items() + ] + ) + + finalize("0074_remove_groupless_users") + + assert_model("user/1", {"meeting_ids": [1], "meeting_user_ids": []}) + assert_model( + "meeting/1", + { + "user_ids": [1], + "meeting_user_ids": [], + }, + ) + assert_model( + "meeting_user/1", + {"user_id": 1, "meeting_id": 1, "group_ids": [], "meta_deleted": True}, + ) + + +def test_only_deleted_target_meeting_users(write, finalize, assert_model): + test_data: dict[str, dict[str, Any]] = { + "user/1": {"meeting_ids": [1], "meeting_user_ids": [1]}, + "meeting/1": { + "user_ids": [1], + "meeting_user_ids": [1], + }, + "meeting_user/1": {"user_id": 1, "meeting_id": 1, "group_ids": []}, + } + + write( + *[ + {"type": "create", "fqid": fqid, "fields": data} + for fqid, data in test_data.items() + ] + ) + write( + {"type": "delete", "fqid": "meeting_user/1"}, + { + "type": "update", + "fqid": "user/1", + "fields": {}, + "list_fields": {"remove": {"meeting_ids": [1], "meeting_user_ids": [1]}}, + }, + { + "type": "update", + "fqid": "meeting/1", + "fields": {}, + "list_fields": {"remove": {"user_ids": [1], "meeting_user_ids": [1]}}, + }, + ) + + finalize("0074_remove_groupless_users") + + assert_model("user/1", {"meeting_ids": [], "meeting_user_ids": []}) + assert_model( + "meeting/1", + { + "user_ids": [], + "meeting_user_ids": [], + }, + ) + assert_model( + "meeting_user/1", + {"user_id": 1, "meeting_id": 1, "group_ids": [], "meta_deleted": True}, + ) + + +def test_no_intact_target_models(write, finalize, assert_model): + test_data: dict[str, dict[str, Any]] = { + "user/1": {"meeting_ids": [1], "meeting_user_ids": [1]}, + "meeting/1": { + "user_ids": [1], + "meeting_user_ids": [1], + }, + "meeting_user/1": {"user_id": 1, "meeting_id": 1, "group_ids": []}, + } + + write( + *[ + {"type": "create", "fqid": fqid, "fields": data} + for fqid, data in test_data.items() + ] + ) + write( + {"type": "delete", "fqid": "meeting/1"}, + { + "type": "update", + "fqid": "user/1", + "fields": {}, + "list_fields": {"remove": {"meeting_ids": [1]}}, + }, + ) + + finalize("0074_remove_groupless_users") + + assert_model("user/1", {"meeting_ids": [], "meeting_user_ids": []}) + assert_model( + "meeting/1", {"user_ids": [1], "meeting_user_ids": [1], "meta_deleted": True} + ) + assert_model( + "meeting_user/1", + {"user_id": 1, "meeting_id": 1, "group_ids": [], "meta_deleted": True}, + ) + + +def test_no_groupless_meeting_users(write, finalize, assert_model): + test_data: dict[str, dict[str, Any]] = { + "user/1": {"meeting_ids": [1], "meeting_user_ids": [1]}, + "meeting/1": {"user_ids": [1], "meeting_user_ids": [1], "group_ids": [1]}, + "meeting_user/1": {"user_id": 1, "meeting_id": 1, "group_ids": [1]}, + "group/1": {"meeting_id": 1, "meeting_user_ids": [1]}, + } + + write( + *[ + {"type": "create", "fqid": fqid, "fields": data} + for fqid, data in test_data.items() + ] + ) + + finalize("0074_remove_groupless_users") + + for fqid, data in test_data.items(): + assert_model( + fqid, + data, + ) + + +def test_migration_complex(write, finalize, assert_model): + to_delete = [ + "user/4", + "meeting/4", + "meeting_user/22", + "motion/14", + "personal_note/4", + "list_of_speakers/41", + "assignment_candidate/4", + "chat_message/8", + "structure_level/4", + "point_of_order_category/3", + "speaker/1111", + "structure_level_list_of_speakers/311", + *[f"{collection}/4" for collection in MOTION_MEETING_USER_MODELS], + ] + collection_to_id_to_data: dict[str, dict[int, dict[str, Any]]] = { + "meeting": { + 1: { + "name": "City hall", + "list_of_speakers_countdown_id": 1, + "projector_countdown_ids": [1], + "group_ids": [1], + "meeting_user_ids": [11, 12, 13, 14, 15, 16], + "motion_ids": [11, 12, 13, 15], + "list_of_speakers_ids": [11, 21, 31, 51], + "personal_note_ids": [3], + "chat_message_ids": [1, 2, 3, 4, 5, 6, 7, 9, 10], + "structure_level_ids": [1, 2, 3], + "point_of_order_category_ids": [1, 2], + "speaker_ids": [ + 1112, + 1113, + 1114, + 2111, + 2115, + 2116, + 3111, + 3116, + 4111, + 5115, + ], + **{ + f"{collection}_ids": [1, 2] + for collection in MOTION_MEETING_USER_MODELS + }, + "structure_level_list_of_speakers_ids": [111, 112, 113, 211, 512], + }, + 2: { + "name": "County congress", + "list_of_speakers_couple_countdown": True, + "list_of_speakers_countdown_id": 2, + "projector_countdown_ids": [2], + "group_ids": [2], + "meeting_user_ids": [21, 23], + "personal_note_ids": [1], + "motion_ids": [21, 22], + "list_of_speakers_ids": [12, 22], + "structure_level_ids": [5, 6], + "speaker_ids": [1221, 1223, 2221], + "structure_level_list_of_speakers_ids": [125, 126], + }, + 3: { + "name": "National congress", + "list_of_speakers_couple_countdown": True, + "list_of_speakers_countdown_id": 3, + "projector_countdown_ids": [3], + "group_ids": [3], + "meeting_user_ids": [31, 32, 33], + "motion_ids": [31], + "list_of_speakers_ids": [13], + "structure_level_ids": [7, 8, 9], + "assignment_candidate_ids": [1, 2, 3], + "speaker_ids": [1331, 1332, 1333], + **{ + f"{collection}_ids": [3] + for collection in MOTION_MEETING_USER_MODELS + }, + "structure_level_list_of_speakers_ids": [139], + }, + 4: { + "name": "Deleted congress", + "list_of_speakers_couple_countdown": True, + }, # delete + }, + "group": { + 1: {"name": "Group A", "meeting_user_ids": [13], "meeting_id": 1}, + 2: {"name": "Group B", "meeting_user_ids": [], "meeting_id": 2}, + 3: {"name": "Group C", "meeting_user_ids": [], "meeting_id": 3}, + }, + "projector_countdown": { + 1: { + "title": "LOS", + "used_as_list_of_speakers_countdown_meeting_id": 1, + "meeting_id": 1, + "default_time": 60, + "countdown_time": 200, + "running": True, + }, + 2: { + "title": "LOS", + "used_as_list_of_speakers_countdown_meeting_id": 2, + "meeting_id": 2, + "default_time": 60, + "countdown_time": 60, + "running": False, + }, + 3: { + "title": "LOS", + "used_as_list_of_speakers_countdown_meeting_id": 3, + "meeting_id": 3, + "default_time": 60, + "countdown_time": 300, + "running": True, + }, + }, + "user": { + 1: {"username": "admin", "meeting_user_ids": [11, 21, 31, 41]}, + 2: {"username": "bob", "meeting_user_ids": [12, 32]}, + 3: {"username": "charlotte", "meeting_user_ids": [13, 23, 33]}, + 4: {"username": "DELETED"}, # delete + 5: {"username": "elizabeth", "meeting_user_ids": [15, 25]}, + 6: {"username": "george", "meeting_user_ids": [16]}, + }, + "meeting_user": { + **get_muser( + 11, + { + "speaker_ids": [1111, 2111, 3111, 4111], + "structure_level_ids": [1], + "chat_message_ids": [9], + }, + ), + **get_muser( + 12, + { + "vote_delegations_from_ids": [13], + "speaker_ids": [1112], + "structure_level_ids": [1, 2], + "group_ids": [], + }, + ), + **get_muser( + 13, + { + "speaker_ids": [1113], + "structure_level_ids": [1, 2], + "chat_message_ids": [], + "group_ids": [1], + "vote_delegated_to_id": 12, + "vote_delegations_from_ids": [14, 15, 16], + }, + ), + **get_muser( + 14, + { + "speaker_ids": [1114], + "structure_level_ids": [1, 2], + "chat_message_ids": [5, 7], + "vote_delegated_to_id": 13, + **{ + f"{collection}_ids": [1] + for collection in MOTION_MEETING_USER_MODELS + }, + }, + ), + **get_muser( + 15, + { + "speaker_ids": [2115, 5115], + "structure_level_ids": [3], + "chat_message_ids": [2, 4, 10], + "personal_note_ids": [3], + "vote_delegated_to_id": 13, + **{ + f"{collection}_ids": [2, 4] + for collection in MOTION_MEETING_USER_MODELS + }, + }, + ), + **get_muser( + 16, + { + "vote_delegated_to_id": 13, + "speaker_ids": [2116, 3116], + "chat_message_ids": [1, 3, 6], + }, + ), + **get_muser( + 21, + { + "speaker_ids": [1221, 2221], + "structure_level_ids": [4, 5], + "assignment_candidate_ids": [4], + "personal_note_ids": [1], + "vote_delegations_from_ids": [22], + }, + ), + **get_muser( + 22, {"vote_delegated_to_id": 21, "vote_delegations_from_ids": [23]} + ), # delete + **get_muser( + 23, + { + "speaker_ids": [1223], + "structure_level_ids": [4], + "personal_note_ids": [2], + "vote_delegated_to_id": 22, + }, + ), + **get_muser(25, {"structure_level_ids": [6], "personal_note_ids": [4]}), + **get_muser( + 31, + { + "speaker_ids": [1331], + "structure_level_ids": [7, 9], + "assignment_candidate_ids": [1], + "vote_delegated_to_id": 32, + **{ + f"{collection}_ids": [3] + for collection in MOTION_MEETING_USER_MODELS + }, + }, + ), + **get_muser( + 32, + { + "speaker_ids": [1332], + "structure_level_ids": [8], + "assignment_candidate_ids": [2], + "vote_delegations_from_ids": [31], + }, + ), + **get_muser( + 33, + { + "speaker_ids": [1333], + "structure_level_ids": [8], + "assignment_candidate_ids": [3], + }, + ), + **get_muser(41), + }, + "list_of_speakers": { + # Opposite id digit order to related motions + **get_los( + 11, + { + "structure_level_list_of_speakers_ids": [111, 112, 113], + "speaker_ids": [1112, 1113, 1114], + }, + ), + **get_los( + 21, + { + "structure_level_list_of_speakers_ids": [211], + "speaker_ids": [2111, 2115, 2116], + }, + ), + **get_los( + 31, + { + "structure_level_list_of_speakers_ids": [], + "speaker_ids": [3111, 3116], + }, + ), + **get_los(41, {"speaker_ids": [4111]}), # delete + **get_los( + 51, + {"structure_level_list_of_speakers_ids": [512], "speaker_ids": [5115]}, + ), + **get_los( + 12, + { + "structure_level_list_of_speakers_ids": [125, 126], + "speaker_ids": [1221, 1223], + }, + ), + **get_los(22, {"speaker_ids": [2221]}), + **get_los( + 13, + { + "structure_level_list_of_speakers_ids": [139], + "speaker_ids": [1331, 1332, 1333], + }, + ), + }, + "motion": { + **get_motion( + 11, + { + **{ + f"{collection[7:]}_ids": [1, 2] + for collection in MOTION_MEETING_USER_MODELS + }, + }, + ), + **get_motion( + 12, + ), + **get_motion( + 13, + { + "personal_note_ids": [3], + }, + ), + **get_motion(14), # delete + **get_motion(15), + **get_motion(21, {"personal_note_ids": [1, 2]}), + **get_motion(22), + **get_motion( + 31, + { + **{ + f"{collection[7:]}_ids": [3] + for collection in MOTION_MEETING_USER_MODELS + } + }, + ), + }, + "personal_note": { + 1: { + "meeting_id": 2, + "content_object_id": "motion/21", + "meeting_user_id": 21, + }, + 2: { + "meeting_id": 2, + "content_object_id": "motion/21", + "meeting_user_id": 23, + }, + 3: { + "meeting_id": 1, + "content_object_id": "motion/13", + "meeting_user_id": 15, + }, + 4: { + "meeting_id": 2, + "content_object_id": "motion/22", + "meeting_user_id": 25, + }, # delete + }, + **{ + collection: { + id_: date + for id_, date in { + 1: { + "meeting_id": 1, + "motion_id": 11, + "meeting_user_id": 14, + **({} if collection == "motion_supporter" else {"weight": 1}), + }, + 2: { + "meeting_id": 1, + "motion_id": 11, + "meeting_user_id": 15, + **({} if collection == "motion_supporter" else {"weight": 2}), + }, + 3: { + "meeting_id": 3, + "motion_id": 31, + "meeting_user_id": 31, + **({} if collection == "motion_supporter" else {"weight": 1}), + }, + 4: { + "meeting_id": 1, + "motion_id": 12, + "meeting_user_id": 15, + "weight": 1, + **({} if collection == "motion_supporter" else {"weight": 1}), + }, # delete + }.items() + } + for collection in MOTION_MEETING_USER_MODELS + }, + "assignment_candidate": { + 1: {"meeting_id": 3, "meeting_user_id": 31}, + 2: {"meeting_id": 3, "meeting_user_id": 32}, + 3: {"meeting_id": 3, "meeting_user_id": 33}, + 4: {"meeting_id": 2, "meeting_user_id": 21}, # delete + }, + "chat_message": { + 1: { + "meeting_user_id": 16, + "meeting_id": 1, + "content": "Hey, any of u up?", + "created": 100, + }, + 2: { + "meeting_user_id": 15, + "meeting_id": 1, + "content": "I am, wazzup dude?", + "created": 200, + }, + 3: { + "meeting_user_id": 16, + "meeting_id": 1, + "content": "Wanna go ditch tomorrows conference and go bar hopping?", + "created": 300, + }, + 4: { + "meeting_user_id": 15, + "meeting_id": 1, + "content": "Absolutely, ma man! Conf's gonna be hella boring anyway...", + "created": 400, + }, + 5: { + "meeting_user_id": 14, + "meeting_id": 1, + "content": "Yo guys, can I join in?", + "created": 500, + }, + 6: { + "meeting_user_id": 16, + "meeting_id": 1, + "content": "Why u even asking, bro? Of course!", + "created": 600, + }, + 7: { + "meeting_user_id": 14, + "meeting_id": 1, + "content": "Totally rad, thx.", + "created": 700, + }, + 8: { + "meeting_user_id": 13, + "meeting_id": 1, + "content": "Srsly? Do ur jobs for once, will ya?", + "created": 1100, + }, # deleted + 9: { + "meeting_user_id": 11, + "meeting_id": 1, + "content": "Have fun everyone, I'll go to the conference and catch you up later.", + "created": 1200, + }, + 10: { + "meeting_user_id": 15, + "meeting_id": 1, + "content": "You da man!", + "created": 1300, + }, + }, + "structure_level": { + 1: { + "meeting_id": 1, + "meeting_user_ids": [11, 12, 13, 14], + "name": "red", + "color": "#ff0000", + "structure_level_list_of_speakers_ids": [111, 211], + }, + 2: { + "meeting_id": 1, + "meeting_user_ids": [12, 13, 14], + "name": "green", + "color": "#00ff00", + "structure_level_list_of_speakers_ids": [112, 512], + }, + 3: { + "meeting_id": 1, + "meeting_user_ids": [15], + "name": "blue", + "color": "#0000ff", + "structure_level_list_of_speakers_ids": [113], + }, + 4: { + "meeting_id": 2, + "meeting_user_ids": [21, 22], + "name": "red", + "color": "#ff0000", + }, # delete + 5: { + "meeting_id": 2, + "meeting_user_ids": [21], + "name": "green", + "color": "#00ff00", + "structure_level_list_of_speakers_ids": [125], + }, + 6: { + "meeting_id": 2, + "meeting_user_ids": [25], + "name": "blue", + "color": "#0000ff", + "structure_level_list_of_speakers_ids": [126], + }, + 7: { + "meeting_id": 3, + "meeting_user_ids": [31], + "name": "red", + "color": "#ff0000", + }, + 8: { + "meeting_id": 3, + "meeting_user_ids": [32, 33], + "name": "green", + "color": "#00ff00", + }, + 9: { + "meeting_id": 3, + "meeting_user_ids": [31], + "name": "blue", + "color": "#0000ff", + "structure_level_list_of_speakers_ids": [139], + }, + }, + "point_of_order_category": { + 1: {"text": "A", "rank": 1, "meeting_id": 1, "speaker_ids": [1112, 1113]}, + 2: {"text": "B", "rank": 2, "meeting_id": 1, "speaker_ids": [2111]}, + 3: { + "text": "C", + "rank": 3, + "meeting_id": 1, + "speaker_ids": [3111], + }, # delete + }, + "speaker": { + # id_ = los_id * 100 + muser_id + **get_speaker( + 1111, + {"structure_level_list_of_speakers_id": 111, "point_of_order": True}, + ), # delete + **get_speaker( + 1112, + { + "structure_level_list_of_speakers_id": 112, + "point_of_order": True, + "point_of_order_category_id": 1, + }, + ), + **get_speaker( + 1113, + { + "structure_level_list_of_speakers_id": 112, + "point_of_order": True, + "point_of_order_category_id": 1, + }, + ), + **get_speaker( + 1114, + { + "structure_level_list_of_speakers_id": 113, + "speech_state": "intervention", + }, + ), + **get_speaker( + 2111, + { + "structure_level_list_of_speakers_id": 211, + "point_of_order": True, + "point_of_order_category_id": 2, + }, + ), + **get_speaker(2115, {"structure_level_list_of_speakers_id": 211}), + **get_speaker( + 2116, + { + "structure_level_list_of_speakers_id": 211, + "speech_state": "interposed_question", + }, + ), + **get_speaker( + 3111, + { + "structure_level_list_of_speakers_id": 311, + "point_of_order": True, + "point_of_order_category_id": 3, + }, + ), + **get_speaker(3116, {"speech_state": "pro"}), + **get_speaker(4111), + **get_speaker( + 5115, + { + "structure_level_list_of_speakers_id": 512, + "begin_time": 100, + "pause_time": 200, + }, + ), + **get_speaker(1221, {"structure_level_list_of_speakers_id": 125}), + **get_speaker(1223, {"structure_level_list_of_speakers_id": 126}), + **get_speaker(2221), + **get_speaker(1331, {"begin_time": 100, "end_time": 200}), + **get_speaker(1332, {"begin_time": 300}), + **get_speaker( + 1333, + {"structure_level_list_of_speakers_id": 139, "speech_state": "contra"}, + ), + }, + "structure_level_list_of_speakers": { + # id_ = los_id * 10 + sl_id + **get_sllos(111, {}), + **get_sllos(112, {"speaker_ids": [1112, 1113]}), + **get_sllos(113, {"speaker_ids": [1114]}), + **get_sllos(211, {"speaker_ids": [2111, 2115, 2116]}), + **get_sllos(311, {"speaker_ids": [3111]}), # delete + **get_sllos(512, {"speaker_ids": [5115]}), + **get_sllos(125, {"speaker_ids": [1221]}), + **get_sllos(126, {"speaker_ids": [1223]}), + **get_sllos(139, {"speaker_ids": [1333]}), + }, + } + write( + *[ + {"type": "create", "fqid": f"{collection}/{id_}", "fields": data} + for collection, id_to_data in collection_to_id_to_data.items() + for id_, data in id_to_data.items() + ] + ) + write( + *[ + { + "type": "delete", + "fqid": fqid, + } + for fqid in to_delete + ] + ) + + finalize("0074_remove_groupless_users") + + assert_model( + "meeting/1", + { + **collection_to_id_to_data["meeting"][1], + "speaker_ids": [1113, 5115], + "meeting_user_ids": [13], + "personal_note_ids": [], + }, + ) + assert_model( + "meeting/2", + { + **collection_to_id_to_data["meeting"][2], + "speaker_ids": [], + "meeting_user_ids": [], + "personal_note_ids": [], + }, + ) + assert_model( + "meeting/3", + { + **collection_to_id_to_data["meeting"][3], + "speaker_ids": [1331, 1332], + "meeting_user_ids": [], + }, + ) + assert_model( + "meeting/4", + { + **collection_to_id_to_data["meeting"][4], + "meta_deleted": True, + }, + ) + + assert_model( + "group/1", {**collection_to_id_to_data["group"][1], "meeting_user_ids": [13]} + ) + assert_model( + "group/2", {**collection_to_id_to_data["group"][2], "meeting_user_ids": []} + ) + assert_model( + "group/3", {**collection_to_id_to_data["group"][3], "meeting_user_ids": []} + ) + + for id_ in range(1, 4): + assert_model( + f"projector_countdown/{id_}", + collection_to_id_to_data["projector_countdown"][id_], + ) + + assert_model("user/1", {"username": "admin", "meeting_user_ids": []}) + assert_model("user/2", {"username": "bob", "meeting_user_ids": []}) + assert_model("user/3", {"username": "charlotte", "meeting_user_ids": [13]}) + assert_model("user/4", {"meta_deleted": True, "username": "DELETED"}) + assert_model("user/5", {"username": "elizabeth", "meeting_user_ids": []}) + assert_model("user/6", {"username": "george", "meeting_user_ids": []}) + + assert_model( + "meeting_user/11", + { + **collection_to_id_to_data["meeting_user"][11], + "meta_deleted": True, + }, + ) + assert_model( + "meeting_user/12", + { + **collection_to_id_to_data["meeting_user"][12], + "meta_deleted": True, + }, + ) + assert_model( + "meeting_user/13", + { + "user_id": 3, + "group_ids": [1], + "meeting_id": 1, + "speaker_ids": [1113], + "chat_message_ids": [], + "structure_level_ids": [1, 2], + "vote_delegations_from_ids": [], + }, + ) + assert_model( + "meeting_user/14", + { + **collection_to_id_to_data["meeting_user"][14], + "meta_deleted": True, + }, + ) + assert_model( + "meeting_user/15", + { + **collection_to_id_to_data["meeting_user"][15], + "meta_deleted": True, + }, + ) + assert_model( + "meeting_user/16", + { + **collection_to_id_to_data["meeting_user"][16], + "meta_deleted": True, + }, + ) + assert_model( + "meeting_user/21", + { + **collection_to_id_to_data["meeting_user"][21], + "meta_deleted": True, + }, + ) + assert_model( + "meeting_user/22", + { + **collection_to_id_to_data["meeting_user"][22], + "meta_deleted": True, + }, + ) + assert_model( + "meeting_user/23", + { + **collection_to_id_to_data["meeting_user"][23], + "meta_deleted": True, + }, + ) + assert_model( + "meeting_user/25", + { + **collection_to_id_to_data["meeting_user"][25], + "meta_deleted": True, + }, + ) + assert_model( + "meeting_user/31", + { + **collection_to_id_to_data["meeting_user"][31], + "meta_deleted": True, + }, + ) + assert_model( + "meeting_user/32", + { + **collection_to_id_to_data["meeting_user"][32], + "meta_deleted": True, + }, + ) + assert_model( + "meeting_user/33", + { + **collection_to_id_to_data["meeting_user"][33], + "meta_deleted": True, + }, + ) + assert_model( + "meeting_user/41", + {**collection_to_id_to_data["meeting_user"][41], "meta_deleted": True}, + ) + + assert_model( + "list_of_speakers/11", + { + **collection_to_id_to_data["list_of_speakers"][11], + "speaker_ids": [1113], + }, + ) + assert_model( + "list_of_speakers/21", + { + **collection_to_id_to_data["list_of_speakers"][21], + "speaker_ids": [], + }, + ) + assert_model( + "list_of_speakers/31", + { + **collection_to_id_to_data["list_of_speakers"][31], + "speaker_ids": [], + }, + ) + assert_model( + "list_of_speakers/41", + { + **collection_to_id_to_data["list_of_speakers"][41], + "meta_deleted": True, + }, + ) + assert_model( + "list_of_speakers/51", + { + **collection_to_id_to_data["list_of_speakers"][51], + "speaker_ids": [5115], + }, + ) + assert_model( + "list_of_speakers/12", + { + **collection_to_id_to_data["list_of_speakers"][12], + "speaker_ids": [], + }, + ) + assert_model( + "list_of_speakers/22", + { + **collection_to_id_to_data["list_of_speakers"][22], + "speaker_ids": [], + }, + ) + assert_model( + "list_of_speakers/13", + { + **collection_to_id_to_data["list_of_speakers"][13], + "speaker_ids": [1331, 1332], + }, + ) + + assert_model( + "motion/11", + { + **collection_to_id_to_data["motion"][11], + }, + ) + assert_model( + "motion/12", + { + **collection_to_id_to_data["motion"][12], + }, + ) + assert_model( + "motion/13", + { + **collection_to_id_to_data["motion"][13], + "personal_note_ids": [], + }, + ) + assert_model( + "motion/14", + { + **collection_to_id_to_data["motion"][14], + "meta_deleted": True, + }, + ) + assert_model("motion/15", {**collection_to_id_to_data["motion"][15]}) + assert_model( + "motion/21", + { + **collection_to_id_to_data["motion"][21], + "personal_note_ids": [], + }, + ) + assert_model("motion/22", {**collection_to_id_to_data["motion"][22]}) + assert_model( + "motion/31", + { + **collection_to_id_to_data["motion"][31], + }, + ) + + for id_ in range(1, 5): + assert_model( + f"personal_note/{id_}", + { + **collection_to_id_to_data["personal_note"][id_], + "meta_deleted": True, + }, + ) + + assert_model("motion_editor/1", {"weight": 1, "motion_id": 11, "meeting_id": 1}) + assert_model("motion_editor/2", {"weight": 2, "motion_id": 11, "meeting_id": 1}) + assert_model("motion_editor/3", {"weight": 1, "motion_id": 31, "meeting_id": 3}) + assert_model( + "motion_editor/4", + { + **collection_to_id_to_data["motion_editor"][4], + "meta_deleted": True, + }, + ) + + assert_model( + "motion_working_group_speaker/1", + {"weight": 1, "motion_id": 11, "meeting_id": 1}, + ) + assert_model( + "motion_working_group_speaker/2", + {"weight": 2, "motion_id": 11, "meeting_id": 1}, + ) + assert_model( + "motion_working_group_speaker/3", + {"weight": 1, "motion_id": 31, "meeting_id": 3}, + ) + assert_model( + "motion_working_group_speaker/4", + { + **collection_to_id_to_data["motion_working_group_speaker"][4], + "meta_deleted": True, + }, + ) + + assert_model("motion_submitter/1", {"weight": 1, "motion_id": 11, "meeting_id": 1}) + assert_model("motion_submitter/2", {"weight": 2, "motion_id": 11, "meeting_id": 1}) + assert_model("motion_submitter/3", {"weight": 1, "motion_id": 31, "meeting_id": 3}) + assert_model( + "motion_submitter/4", + { + **collection_to_id_to_data["motion_submitter"][4], + "meta_deleted": True, + }, + ) + + assert_model("motion_supporter/1", {"motion_id": 11, "meeting_id": 1}) + assert_model("motion_supporter/2", {"motion_id": 11, "meeting_id": 1}) + assert_model("motion_supporter/3", {"motion_id": 31, "meeting_id": 3}) + assert_model( + "motion_supporter/4", + { + **collection_to_id_to_data["motion_supporter"][4], + "meta_deleted": True, + }, + ) + + assert_model("assignment_candidate/1", {"meeting_id": 3}) + assert_model("assignment_candidate/2", {"meeting_id": 3}) + assert_model("assignment_candidate/3", {"meeting_id": 3}) + assert_model( + "assignment_candidate/4", + {**collection_to_id_to_data["assignment_candidate"][4], "meta_deleted": True}, + ) + + assert_model( + "chat_message/1", + {"content": "Hey, any of u up?", "created": 100, "meeting_id": 1}, + ) + assert_model( + "chat_message/2", + {"content": "I am, wazzup dude?", "created": 200, "meeting_id": 1}, + ) + assert_model( + "chat_message/3", + { + "content": "Wanna go ditch tomorrows conference and go bar hopping?", + "created": 300, + "meeting_id": 1, + }, + ) + assert_model( + "chat_message/4", + { + "content": "Absolutely, ma man! Conf's gonna be hella boring anyway...", + "created": 400, + "meeting_id": 1, + }, + ) + assert_model( + "chat_message/5", + {"content": "Yo guys, can I join in?", "created": 500, "meeting_id": 1}, + ) + assert_model( + "chat_message/6", + { + "content": "Why u even asking, bro? Of course!", + "created": 600, + "meeting_id": 1, + }, + ) + assert_model( + "chat_message/7", + {"content": "Totally rad, thx.", "created": 700, "meeting_id": 1}, + ) + assert_model( + "chat_message/8", + { + **collection_to_id_to_data["chat_message"][8], + "meta_deleted": True, + }, + ) + assert_model( + "chat_message/9", + { + "content": "Have fun everyone, I'll go to the conference and catch you up later.", + "created": 1200, + "meeting_id": 1, + }, + ) + assert_model( + "chat_message/10", {"content": "You da man!", "created": 1300, "meeting_id": 1} + ) + + assert_model( + "structure_level/1", + { + **collection_to_id_to_data["structure_level"][1], + "meeting_user_ids": [13], + }, + ) + assert_model( + "structure_level/2", + { + **collection_to_id_to_data["structure_level"][2], + "meeting_user_ids": [13], + }, + ) + assert_model( + "structure_level/3", + { + **collection_to_id_to_data["structure_level"][3], + "meeting_user_ids": [], + }, + ) + assert_model( + "structure_level/4", + { + **collection_to_id_to_data["structure_level"][4], + "meta_deleted": True, + }, + ) + for id_ in range(5, 10): + assert_model( + f"structure_level/{id_}", + { + **collection_to_id_to_data["structure_level"][id_], + "meeting_user_ids": [], + }, + ) + + assert_model( + "point_of_order_category/1", + { + **collection_to_id_to_data["point_of_order_category"][1], + "speaker_ids": [1113], + }, + ) + assert_model( + "point_of_order_category/2", + {**collection_to_id_to_data["point_of_order_category"][2], "speaker_ids": []}, + ) + assert_model( + "point_of_order_category/3", + { + **collection_to_id_to_data["point_of_order_category"][3], + "meta_deleted": True, + }, + ) + + for id_ in [ + 1111, + 1112, + 1114, + 2111, + 2115, + 2116, + 3111, + 3116, + 4111, + 1221, + 1223, + 2221, + 1333, + ]: + assert_model( + f"speaker/{id_}", + { + **collection_to_id_to_data["speaker"][id_], + "meta_deleted": True, + }, + ) + assert_model( + "speaker/1113", + collection_to_id_to_data["speaker"][1113], + ) + assert_model( + "speaker/5115", + { + "begin_time": 100, + "meeting_id": 1, + "pause_time": 200, + "list_of_speakers_id": 51, + "structure_level_list_of_speakers_id": 512, + }, + ) + assert_model( + "speaker/1331", + { + "end_time": 200, + "begin_time": 100, + "meeting_id": 3, + "list_of_speakers_id": 13, + }, + ) + assert_model( + "speaker/1332", {"begin_time": 300, "meeting_id": 3, "list_of_speakers_id": 13} + ) + + assert_model( + "structure_level_list_of_speakers/111", + collection_to_id_to_data["structure_level_list_of_speakers"][111], + ) + assert_model( + "structure_level_list_of_speakers/112", + { + **collection_to_id_to_data["structure_level_list_of_speakers"][112], + "speaker_ids": [1113], + }, + ) + assert_model( + "structure_level_list_of_speakers/113", + { + **collection_to_id_to_data["structure_level_list_of_speakers"][113], + "speaker_ids": [], + }, + ) + assert_model( + "structure_level_list_of_speakers/211", + { + **collection_to_id_to_data["structure_level_list_of_speakers"][211], + "speaker_ids": [], + }, + ) + assert_model( + "structure_level_list_of_speakers/311", + { + **collection_to_id_to_data["structure_level_list_of_speakers"][311], + "meta_deleted": True, + }, + ) + assert_model( + "structure_level_list_of_speakers/512", + { + **collection_to_id_to_data["structure_level_list_of_speakers"][512], + "speaker_ids": [5115], + }, + ) + assert_model( + "structure_level_list_of_speakers/125", + { + **collection_to_id_to_data["structure_level_list_of_speakers"][125], + "speaker_ids": [], + }, + ) + assert_model( + "structure_level_list_of_speakers/126", + { + **collection_to_id_to_data["structure_level_list_of_speakers"][126], + "speaker_ids": [], + }, + ) + assert_model( + "structure_level_list_of_speakers/139", + { + **collection_to_id_to_data["structure_level_list_of_speakers"][139], + "speaker_ids": [], + }, + ) diff --git a/tests/system/presenter/test_get_user_related_models.py b/tests/system/presenter/test_get_user_related_models.py index 4fe09c80b5..e9a5b39c3f 100644 --- a/tests/system/presenter/test_get_user_related_models.py +++ b/tests/system/presenter/test_get_user_related_models.py @@ -179,7 +179,7 @@ def test_two_meetings(self) -> None: ) # Admin groups of meeting/1 for requesting user # 111 into both meetings - self.move_user_to_group({12: 2, 42: None, 1111: 1, 4111: 4}) + self.move_user_to_group({12: 2, 1111: 1, 4111: 4}) status_code, data = self.request("get_user_related_models", {"user_ids": [111]}) self.assertEqual(status_code, 403) self.assertEqual(