diff --git a/openslides_backend/action/actions/meeting/clone.py b/openslides_backend/action/actions/meeting/clone.py index 89fe4934d4..f04cb1eb53 100644 --- a/openslides_backend/action/actions/meeting/clone.py +++ b/openslides_backend/action/actions/meeting/clone.py @@ -268,6 +268,7 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: raise ActionException( "Cannot create a non-template meeting without administrators" ) + self.transform_json_fields(instance) return instance def _create_or_get_meeting_user(self, meeting_id: int, user_id: int) -> int: diff --git a/openslides_backend/action/actions/meeting/import_.py b/openslides_backend/action/actions/meeting/import_.py index bdefeb6d66..e30b040dbe 100644 --- a/openslides_backend/action/actions/meeting/import_.py +++ b/openslides_backend/action/actions/meeting/import_.py @@ -4,6 +4,8 @@ from datetime import datetime from typing import Any +from psycopg.types.json import Jsonb + from openslides_backend.action.actions.meeting.mixins import MeetingPermissionMixin from openslides_backend.migrations.migration_helper import MigrationHelper from openslides_backend.models.base import model_registry @@ -13,9 +15,11 @@ BaseRelationField, GenericRelationField, GenericRelationListField, + JSONField, OnDelete, RelationField, RelationListField, + TimestampField, ) from openslides_backend.models.models import Meeting from openslides_backend.permissions.management_levels import OrganizationManagementLevel @@ -161,9 +165,42 @@ def preprocess_data(self, instance: dict[str, Any]) -> dict[str, Any]: self.remove_not_allowed_fields(instance) self.set_committee_and_orga_relation(instance) self.check_data_migration_index(instance) + self.transform_timestamps(instance) self.unset_committee_and_orga_relation(instance) return instance + def transform_timestamps(self, instance: dict[str, Any]) -> dict[str, Any]: + for collection, collection_data in instance["meeting"].items(): + if model := model_registry.get(collection): + fields = list(model().get_fields()) + timestamp_field_names = [ + field.own_field_name + for field in fields + if isinstance(field, TimestampField) + ] + if timestamp_field_names: + for mod in collection_data.values(): + for field in timestamp_field_names: + if (iso := mod.get(field)) and isinstance(iso, str): + mod[field] = datetime.fromisoformat(iso) + return instance + + def transform_json_fields(self, instance: dict[str, Any]) -> dict[str, Any]: + for collection, collection_data in instance["meeting"].items(): + if model := model_registry.get(collection): + fields = list(model().get_fields()) + json_field_names = [ + field.own_field_name + for field in fields + if isinstance(field, JSONField) + ] + if json_field_names: + for mod in collection_data.values(): + for field in json_field_names: + if field in mod: + mod[field] = Jsonb(mod[field]) + return instance + def check_one_meeting(self, instance: dict[str, Any]) -> None: if len(instance["meeting"]["meeting"]) != 1: raise ActionException("Need exactly one meeting in meeting collection.") @@ -276,6 +313,7 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: raise ActionException(str(ce)) self.allowed_collections = checker.allowed_collections + self.transform_json_fields(instance) self.check_limit_of_meetings() self.update_meeting_and_users(instance) @@ -752,7 +790,7 @@ def check_data_migration_index(self, instance: dict[str, Any]) -> None: """ Check for valid migration index. """ - start_migration_index = instance["meeting"].pop("_migration_index") + start_migration_index = instance["meeting"].get("_migration_index") backend_migration_index = MigrationHelper.get_backend_migration_index() if backend_migration_index < start_migration_index: raise ActionException( diff --git a/openslides_backend/models/fields.py b/openslides_backend/models/fields.py index bebc49dca0..eed3ed0307 100644 --- a/openslides_backend/models/fields.py +++ b/openslides_backend/models/fields.py @@ -1,5 +1,5 @@ from datetime import datetime -from decimal import Decimal +from decimal import Decimal, InvalidOperation from enum import StrEnum from typing import Any, cast @@ -253,7 +253,12 @@ def validate(self, value: Any, payload: dict[str, Any] = {}) -> Any: if value is not None or self.required: if (min_ := self.constraints.get("minimum")) is not None: if isinstance(value, str): - value = Decimal(value) + try: + value = Decimal(value) + except InvalidOperation: + raise ActionException( + f"{self.own_field_name}: value '{value}' couldn't be converted to decimal." + ) elif not isinstance(value, Decimal | None): raise NotImplementedError( f"Unexpected type: {type(value)} (value: {value}) for field {self.get_own_field_name()}" diff --git a/openslides_backend/presenter/export_meeting.py b/openslides_backend/presenter/export_meeting.py index a6df59c5b2..176e1e7324 100644 --- a/openslides_backend/presenter/export_meeting.py +++ b/openslides_backend/presenter/export_meeting.py @@ -40,7 +40,9 @@ def get_result(self) -> Any: msg = "You are not allowed to perform presenter export_meeting." msg += f" Missing permission: {OrganizationManagementLevel.SUPERADMIN}" raise PermissionDenied(msg) - export_data = export_meeting(self.datastore, self.data["meeting_id"]) + export_data = export_meeting( + self.datastore, self.data["meeting_id"], datetime_decimal_to_string=True + ) if id_ := next( ( id_ diff --git a/openslides_backend/shared/export_helper.py b/openslides_backend/shared/export_helper.py index ca644791b0..5d8b95db10 100644 --- a/openslides_backend/shared/export_helper.py +++ b/openslides_backend/shared/export_helper.py @@ -1,4 +1,6 @@ +import datetime from collections.abc import Iterable +from decimal import Decimal from typing import Any from openslides_backend.migrations.migration_helper import MigrationHelper @@ -34,6 +36,7 @@ def export_meeting( meeting_id: int, internal_target: bool = False, update_mediafiles: bool = False, + datetime_decimal_to_string: bool = False, ) -> dict[str, Any]: export: dict[str, Any] = {} @@ -212,6 +215,14 @@ def export_meeting( export[collection] = dict( sorted(instances.items(), key=lambda item: int(item[0])) ) + if datetime_decimal_to_string and isinstance(instances, dict): + for data in instances.values(): + for field, value in data.items(): + if isinstance(value, datetime.datetime): + data[field] = value.isoformat() + if isinstance(value, Decimal): + data[field] = str(value) + return export diff --git a/tests/system/action/meeting/test_clone.py b/tests/system/action/meeting/test_clone.py index a53e4df526..907ac551f4 100644 --- a/tests/system/action/meeting/test_clone.py +++ b/tests/system/action/meeting/test_clone.py @@ -1372,11 +1372,11 @@ def test_meeting_name_exact_fit(self) -> None: self.assert_model_exists("meeting/2", {"name": long_name + " - Copy"}) def test_meeting_name_too_long(self) -> None: - self.meeting_data["name"] = "A" * 100 + self.meeting_data["name"] = "A" * 200 self.set_test_data_with_admin() response = self.request("meeting.clone", {"meeting_id": 1}) self.assert_status_code(response, 200) - self.assert_model_exists("meeting/2", {"name": "A" * 90 + "... - Copy"}) + self.assert_model_exists("meeting/2", {"name": "A" * 190 + "... - Copy"}) def test_permissions_explicit_source_committee_permission(self) -> None: self.set_test_data() @@ -1841,6 +1841,21 @@ def test_clone_amendment_paragraphs(self) -> None: response.json["message"], ) + def test_clone_amendment_paragraphs_regular(self) -> None: + self.set_test_data() + self.set_user_groups(1, [1]) + self.create_motion( + 1, 1, motion_data={"amendment_paragraphs": Jsonb({"1": "

test

"})} + ) + response = self.request( + "meeting.clone", + { + "meeting_id": 1, + "admin_ids": [1], + }, + ) + self.assert_status_code(response, 200) + def test_permissions_oml_locked_meeting(self) -> None: self.create_meeting( meeting_data={"locked_from_inside": True, "template_for_organization_id": 1} @@ -2134,3 +2149,15 @@ def test_clone_with_structured_published_orga_files(self) -> None: for fqid, model in models.items(): self.assert_model_exists(fqid, model) self.media.duplicate_mediafile.assert_not_called() + + def test_clone_require_duplicate_from_allowed(self) -> None: + self.set_test_data_with_admin() + self.set_models( + { + "meeting/1": {"template_for_organization_id": 1, "name": "m1"}, + } + ) + self.set_committee_management_level([60]) + self.set_organization_management_level(None) + response = self.request("meeting.clone", {"meeting_id": 1}) + self.assert_status_code(response, 200) diff --git a/tests/system/action/meeting/test_import.py b/tests/system/action/meeting/test_import.py index e0c75cebbd..98eae058fe 100644 --- a/tests/system/action/meeting/test_import.py +++ b/tests/system/action/meeting/test_import.py @@ -3,11 +3,11 @@ from copy import deepcopy from typing import Any -import pytest from psycopg.types.json import Jsonb from openslides_backend.action.action_worker import ActionWorkerState from openslides_backend.http.views.presenter_view import PresenterView +from openslides_backend.migrations.migration_helper import MigrationHelper from openslides_backend.models.models import Meeting from openslides_backend.shared.util import ONE_ORGANIZATION_FQID, get_initial_data_file from tests.system.action.base import BaseActionTestCase @@ -20,10 +20,10 @@ from tests.util import Client -@pytest.mark.skip(reason="Requires initial migration. TODO: unskip once it is added.") class MeetingImport(BaseActionTestCase): def setUp(self) -> None: super().setUp() + self.mig_index = MigrationHelper.get_backend_migration_index() self.create_meeting(1, {"external_id": "ext_id"}) self.create_motion(1, 1, motion_data={"number_value": 31}) self.set_models( @@ -39,6 +39,7 @@ def create_request_data( data: dict[str, Any] = { "committee_id": 60, "meeting": { + "_migration_index": self.mig_index, "meeting": { "1": { "id": 1, @@ -58,8 +59,8 @@ def create_request_data( "template_for_organization_id": None, "enable_anonymous": False, "location": "", - "start_time": 10, - "end_time": 10, + "start_time": "1989-11-09T19:00:00+01:00", + "end_time": "1990-10-03T00:00:00+01:00", "welcome_title": "Welcome to OpenSlides", "welcome_text": "[Space for your welcome text.]", "conference_show": False, @@ -390,8 +391,8 @@ def get_motion_data(self, obj_id: int, data: dict[str, Any] = {}) -> dict[str, A "state_extension": "

regeer

", "recommendation_extension": None, "sort_weight": 10000, - "created": 1584512346, - "last_modified": 1584512346, + "created": "1990-07-06T00:00:00+01:00", + "last_modified": "1990-07-22T12:00:00+01:00", "start_line_number": 1, **data, } @@ -410,7 +411,7 @@ def get_mediafile_data( "filename": "A.txt", "mimetype": "text/plain", "pdf_information": {}, - "create_timestamp": 1584513771, + "create_timestamp": "1990-07-22T12:00:00+01:00", "parent_id": None, "child_ids": [], "meeting_mediafile_ids": [obj_id], @@ -449,6 +450,7 @@ def test_no_meeting_collection(self) -> None: { "committee_id": 1, "meeting": { + "_migration_index": self.mig_index, "meeting": {}, }, }, @@ -465,6 +467,7 @@ def test_too_many_meeting_collections(self) -> None: { "committee_id": 1, "meeting": { + "_migration_index": self.mig_index, "meeting": {"1": {"id": 1}, "2": {"id": 2}}, }, }, @@ -561,18 +564,18 @@ def test_replace_ids_and_write_to_datastore(self) -> None: { "name": "Test", "description": "blablabla", - "committee_id": 1, + "committee_id": 60, "enable_anonymous": False, "is_active_in_organization_id": 1, }, ) - assert start <= meeting_2.get("imported_at", 0) <= end + assert start <= round(meeting_2.get("imported_at", 0).timestamp()) <= end user_2 = self.assert_model_exists( "user/2", { "username": "test", "meeting_ids": [2], - "committee_ids": [1], + "committee_ids": [60], "meeting_user_ids": [1], }, ) @@ -584,17 +587,17 @@ def test_replace_ids_and_write_to_datastore(self) -> None: "user_id": 2, "structure_level_ids": [1], "personal_note_ids": [1], - "motion_submitter_ids": [], - "group_ids": [2], + "motion_submitter_ids": None, + "group_ids": [4], }, ) self.assert_model_exists( - "meeting_user/2", {"meeting_id": 2, "user_id": 1, "group_ids": [2]} + "meeting_user/2", {"meeting_id": 2, "user_id": 1, "group_ids": [4]} ) self.assert_model_exists( - "projector/2", {"meeting_id": 2, "sequential_number": 2} + "projector/2", {"meeting_id": 2, "sequential_number": 1} ) - self.assert_model_exists("group/2", {"meeting_user_ids": [1, 2]}) + self.assert_model_exists("group/4", {"meeting_user_ids": [1, 2]}) self.assert_model_exists( "personal_note/1", {"content_object_id": "motion/2", "meeting_user_id": 1, "meeting_id": 2}, @@ -606,7 +609,7 @@ def test_replace_ids_and_write_to_datastore(self) -> None: "structure_level/1", {"meeting_user_ids": [1], "name": "meeting freak"} ) self.assert_model_exists( - "committee/1", {"user_ids": [2, 1], "meeting_ids": [1, 2]} + "committee/60", {"user_ids": [1, 2], "meeting_ids": [1, 2]} ) self.assert_model_exists(ONE_ORGANIZATION_FQID, {"active_meeting_ids": [1, 2]}) @@ -699,6 +702,7 @@ def test_check_usernames_1(self) -> None: del request_data["meeting"]["meeting_user"]["11"] request_data["meeting"]["meeting"]["1"]["admin_group_id"] = 1111 request_data["meeting"]["meeting"]["1"]["meeting_user_ids"] = [111] + request_data["meeting"]["meeting"]["1"]["user_ids"] = [11] request_data["meeting"]["meeting"]["1"]["group_ids"] = [2, 1111] req_user = request_data["meeting"]["user"]["11"] self.set_models( @@ -718,13 +722,13 @@ def test_check_usernames_1(self) -> None: imported_meeting = self.assert_model_exists( "meeting/2", { - "group_ids": [2, 3], - "committee_id": 1, + "group_ids": [4, 5], + "committee_id": 60, "projector_ids": [2], - "admin_group_id": 3, - "default_group_id": 2, - "motion_state_ids": [1], - "motion_workflow_ids": [1], + "admin_group_id": 5, + "default_group_id": 4, + "motion_state_ids": [2], + "motion_workflow_ids": [2], "is_active_in_organization_id": 1, }, ) @@ -747,7 +751,7 @@ def test_check_usernames_1(self) -> None: { "meeting_id": 2, "user_id": 1, - "group_ids": [3], + "group_ids": [5], "comment": "imported user111 for external meeting1", }, ) @@ -757,20 +761,20 @@ def test_check_usernames_1(self) -> None: "group/1", { "meeting_id": 1, - "name": "group1_m1", + "name": "group1", }, ) self.assert_model_exists( - "group/2", + "group/4", { "name": "imported default group2", - "meeting_user_ids": [], + "meeting_user_ids": None, "meeting_id": 2, "default_group_for_meeting_id": 2, }, ) self.assert_model_exists( - "group/3", + "group/5", { "name": "group1111", "meeting_user_ids": [1], @@ -801,6 +805,7 @@ def test_check_usernames_2(self) -> None: "last_name": "admin1", }, ) + request_data["meeting"]["meeting"]["1"]["user_ids"] = [1, 2] response = self.request("meeting.import", request_data) self.assert_status_code(response, 200) @@ -820,13 +825,13 @@ def test_check_usernames_2(self) -> None: "user/3", {"username": "admin11", "last_name": "admin1"} ) self.assert_model_exists( - "meeting_user/1", {"meeting_id": 2, "user_id": 2, "group_ids": [2]} + "meeting_user/1", {"meeting_id": 2, "user_id": 2, "group_ids": [4]} ) self.assert_model_exists( - "meeting_user/2", {"meeting_id": 2, "user_id": 1, "group_ids": [2]} + "meeting_user/2", {"meeting_id": 2, "user_id": 1, "group_ids": [4]} ) self.assert_model_exists( - "group/2", {"meeting_user_ids": [1, 2], "meeting_id": 2} + "group/4", {"meeting_user_ids": [1, 2], "meeting_id": 2} ) def test_check_usernames_new_and_twice(self) -> None: @@ -963,13 +968,13 @@ def test_double_import(self) -> None: ) self.assert_model_exists( "meeting_user/1", - {"user_id": 2, "meeting_id": 2, "group_ids": [2], "personal_note_ids": [1]}, + {"user_id": 2, "meeting_id": 2, "group_ids": [4], "personal_note_ids": [1]}, ) self.assert_model_exists( - "meeting_user/2", {"user_id": 1, "meeting_id": 2, "group_ids": [2]} + "meeting_user/2", {"user_id": 1, "meeting_id": 2, "group_ids": [4]} ) self.assert_model_exists( - "group/2", + "group/4", { "meeting_user_ids": [1, 2], "meeting_id": 2, @@ -977,9 +982,9 @@ def test_double_import(self) -> None: }, ) self.assert_model_exists( - "group/3", + "group/5", { - "meeting_user_ids": [], + "meeting_user_ids": None, "meeting_id": 2, "default_group_for_meeting_id": 2, }, @@ -997,35 +1002,37 @@ def test_double_import(self) -> None: "username": "test", "meeting_user_ids": [1, 3], "meeting_ids": [2, 3], - "committee_ids": [1], + "committee_ids": [60], }, ) self.assert_model_exists( "meeting_user/3", - {"user_id": 2, "meeting_id": 3, "group_ids": [4], "personal_note_ids": [2]}, + {"user_id": 2, "meeting_id": 3, "group_ids": [6], "personal_note_ids": [2]}, ) self.assert_model_exists( - "meeting_user/4", {"user_id": 1, "meeting_id": 3, "group_ids": [4]} + "meeting_user/4", {"user_id": 1, "meeting_id": 3, "group_ids": [6]} ) meeting_3 = self.assert_model_exists( "meeting/3", { "name": "Test", "description": "blablabla", - "committee_id": 1, + "committee_id": 60, "enable_anonymous": False, - "user_ids": [2, 1], - "group_ids": [4, 5], + "user_ids": [1, 2], + "group_ids": [6, 7], "meeting_user_ids": [3, 4], }, ) - assert start <= meeting_3.get("imported_at", 0) <= start + 300 + assert ( + start <= round(meeting_3.get("imported_at", 0).timestamp()) <= start + 300 + ) self.assert_model_exists( - "projector/3", {"meeting_id": 3, "sequential_number": 3} + "projector/3", {"meeting_id": 3, "sequential_number": 1} ) self.assert_model_exists( - "group/4", + "group/6", { "meeting_user_ids": [3, 4], "meeting_id": 3, @@ -1033,10 +1040,10 @@ def test_double_import(self) -> None: }, ) self.assert_model_exists( - "group/5", + "group/7", { "name": "imported default group2", - "meeting_user_ids": [], + "meeting_user_ids": None, "meeting_id": 3, "default_group_for_meeting_id": 3, }, @@ -1048,7 +1055,7 @@ def test_double_import(self) -> None: "tag/2", {"tagged_ids": ["motion/3"], "name": "testag", "meeting_id": 3} ) self.assert_model_exists( - "committee/1", {"user_ids": [2, 1], "meeting_ids": [1, 2, 3]} + "committee/60", {"user_ids": [1, 2], "meeting_ids": [1, 2, 3]} ) def test_no_permission(self) -> None: @@ -1160,11 +1167,9 @@ def test_meeting_user_ids(self) -> None: # User/1 is in user_ids, because calling user is added response = self.request("meeting.import", self.create_request_data({})) self.assert_status_code(response, 200) - # XXX meeting2 = self.assert_model_exists("meeting/2") - # XXX self.assertCountEqual(meeting2["user_ids"], [1, 2]) - # self.assert_model_exists("user/2", {"username": "test", "meeting_ids": [2]}) - organization = self.assert_model_exists("organization/1") - self.assertCountEqual(organization.get("user_ids", []), [1, 2]) + self.assert_model_exists("meeting/2", {"user_ids": [1, 2]}) + self.assert_model_exists("user/2", {"username": "test", "meeting_ids": [2]}) + self.assert_model_exists("organization/1", {"user_ids": [1, 2]}) def test_motion_recommendation_extension(self) -> None: # Special field @@ -1222,7 +1227,7 @@ def test_motion_recommendation_extension(self) -> None: "state_extension_reference_ids": ["motion/2"], "recommendation_extension": "bla[motion/2]bla", "recommendation_extension_reference_ids": ["motion/2"], - "sequential_number": 3, + "sequential_number": 2, }, ) @@ -1393,23 +1398,31 @@ def test_request_user_in_admin_group(self) -> None: "user/1", {"meeting_user_ids": [2], "username": "admin"} ) self.assert_model_exists( - "meeting_user/2", {"group_ids": [2], "meeting_id": 2, "user_id": 1} + "meeting_user/2", {"group_ids": [4], "meeting_id": 2, "user_id": 1} ) self.assert_model_exists( "user/2", {"meeting_user_ids": [1], "username": "test"} ) self.assert_model_exists( - "meeting_user/1", {"group_ids": [2], "meeting_id": 2, "user_id": 2} + "meeting_user/1", {"group_ids": [4], "meeting_id": 2, "user_id": 2} ) - self.assert_model_exists("meeting/2", {"user_ids": [2, 1]}) + self.assert_model_exists("meeting/2", {"user_ids": [1, 2]}) self.assert_model_exists( - "group/2", + "group/4", { "meeting_user_ids": [1, 2], "meeting_id": 2, "name": "imported admin group1", }, ) + self.assert_model_exists( + "group/5", + { + "name": "imported default group2", + "meeting_user_ids": None, + "default_group_for_meeting_id": 2, + }, + ) def test_motion_all_derived_motion_ids(self) -> None: """ @@ -1454,7 +1467,7 @@ def test_motion_all_derived_motion_ids(self) -> None: "derived_motion_ids": None, "all_origin_ids": None, "all_derived_motion_ids": None, - "sequential_number": 2, + "sequential_number": 1, }, ) @@ -1509,11 +1522,11 @@ def test_motion_all_origin_ids(self) -> None: response = self.request("meeting.import", request_data) self.assert_status_code(response, 200) motion = self.assert_model_exists( - "motion/2", {"meeting_id": 2, "sequential_number": 2} + "motion/2", {"meeting_id": 2, "sequential_number": 1} ) assert motion.get("all_origin_ids") is None motion = self.assert_model_exists( - "motion/3", {"meeting_id": 2, "sequential_number": 3} + "motion/3", {"meeting_id": 2, "sequential_number": 2} ) assert motion.get("all_derived_motion_ids") is None @@ -1555,7 +1568,7 @@ def test_foreign_motion_all_origin_ids(self) -> None: "meeting_id": 2, "all_origin_ids": None, "origin_meeting_id": None, - "sequential_number": 2, + "sequential_number": 1, }, ) @@ -1721,10 +1734,10 @@ def test_check_hit_limit_of_users(self) -> None: def test_merge_users_check_committee_and_meeting(self) -> None: self.set_models( { - "committee/1": { + "committee/60": { "user_ids": [1, 14], }, - "committee/2": { + "committee/61": { "name": "Committee for imported meeting", }, "meeting/1": { @@ -1771,6 +1784,7 @@ def test_merge_users_check_committee_and_meeting(self) -> None: "username": "username_to_merge", "email": "test@example.de", "meeting_user_ids": [12], + "meeting_ids": [1], "organization_id": 1, }, "13": { @@ -1778,6 +1792,7 @@ def test_merge_users_check_committee_and_meeting(self) -> None: "username": "username_import13", "email": "test_new@example.de", "meeting_user_ids": [13], + "meeting_ids": [1], "organization_id": 1, }, }, @@ -1802,7 +1817,7 @@ def test_merge_users_check_committee_and_meeting(self) -> None: request_data["meeting"]["meeting"]["1"]["meeting_user_ids"] = [11, 12, 13] request_data["meeting"]["meeting"]["1"]["user_ids"] = [1, 12, 13] request_data["meeting"]["user"]["1"]["username"] = "username_import1" - request_data["committee_id"] = 2 + request_data["committee_id"] = 61 response = self.request("meeting.import", request_data) self.assert_status_code(response, 200) assert response.json["results"][0][0]["number_of_imported_users"] == 3 @@ -1812,7 +1827,7 @@ def test_merge_users_check_committee_and_meeting(self) -> None: { "username": "admin", "meeting_ids": [1, 2], - "committee_ids": [1, 2], + "committee_ids": [60, 61], "meeting_user_ids": [1, 18], }, ) @@ -1821,7 +1836,7 @@ def test_merge_users_check_committee_and_meeting(self) -> None: { "username": "username_to_merge", "meeting_ids": [1, 2], - "committee_ids": [1, 2], + "committee_ids": [60, 61], "meeting_user_ids": [14, 16], }, ) @@ -1830,7 +1845,7 @@ def test_merge_users_check_committee_and_meeting(self) -> None: { "username": "username_import1", "meeting_ids": [2], - "committee_ids": [2], + "committee_ids": [61], "meeting_user_ids": [15], }, ) @@ -1839,21 +1854,27 @@ def test_merge_users_check_committee_and_meeting(self) -> None: { "username": "username_import13", "meeting_ids": [2], - "committee_ids": [2], + "committee_ids": [61], "meeting_user_ids": [17], }, ) - committee1 = self.assert_model_exists("committee/1", {"meeting_ids": [1]}) - assert sorted(committee1.get("user_ids", [])) == [1, 14] - meeting1 = self.assert_model_exists("meeting/1", {"committee_id": 1}) - assert sorted(meeting1.get("user_ids", [])) == [1, 14] - assert sorted(meeting1.get("meeting_user_ids", [])) == [1, 14] - self.assert_model_exists("committee/2", {"meeting_ids": [2]}) - self.assert_model_exists("meeting/2", {"committee_id": 2}) - organization = self.assert_model_exists( - "organization/1", {"committee_ids": [1, 2], "active_meeting_ids": [1, 2]} + self.assert_model_exists( + "committee/60", {"meeting_ids": [1], "user_ids": [1, 14]} + ) + self.assert_model_exists( + "meeting/1", + {"committee_id": 60, "user_ids": [1, 14], "meeting_user_ids": [1, 14]}, + ) + self.assert_model_exists("committee/61", {"meeting_ids": [2]}) + self.assert_model_exists("meeting/2", {"committee_id": 61}) + self.assert_model_exists( + "organization/1", + { + "committee_ids": [60, 61], + "active_meeting_ids": [1, 2], + "user_ids": [1, 14, 15, 16], + }, ) - assert sorted(organization.get("user_ids", [])) == [1, 14, 15, 16] def test_merge_users_check_user_meeting_ids(self) -> None: self.set_models( @@ -1905,14 +1926,15 @@ def test_merge_users_check_user_meeting_ids(self) -> None: ) request_data["meeting"]["group"]["1"]["meeting_user_ids"] = [11, 12] request_data["meeting"]["meeting"]["1"]["meeting_user_ids"] = [11, 12] + request_data["meeting"]["meeting"]["1"]["user_ids"] = [1, 12] response = self.request("meeting.import", request_data) self.assert_status_code(response, 200) assert response.json["results"][0][0]["number_of_imported_users"] == 2 assert response.json["results"][0][0]["number_of_merged_users"] == 1 self.assert_model_exists( - "committee/1", {"meeting_ids": [1, 2], "user_ids": [15, 14, 1]} + "committee/60", {"meeting_ids": [1, 2], "user_ids": [1, 14, 15]} ) - meeting2 = self.assert_model_exists("meeting/2", {"committee_id": 1}) + meeting2 = self.assert_model_exists("meeting/2", {"committee_id": 60}) assert sorted(meeting2.get("user_ids", [])) == [1, 14, 15] self.assert_model_exists("meeting/1", {"user_ids": [14]}) self.assert_model_exists("user/1", {"username": "admin", "meeting_ids": [2]}) @@ -2049,8 +2071,18 @@ def test_merge_meeting_users_fields(self) -> None: "personal_note_ids": [1], "motion_submitter_ids": [], "vote_delegated_to_id": 1, + "group_ids": [1], + }, + "user/1": { + "meeting_user_ids": [1], }, - "group/1": {"meeting_user_ids": [14]}, + "meeting_user/1": { + "meeting_id": 1, + "user_id": 1, + "vote_delegations_from_ids": [14], + "group_ids": [1], + }, + "group/1": {"meeting_user_ids": [1, 14]}, "personal_note/1": { "meeting_id": 1, "content_object_id": None, @@ -2060,7 +2092,7 @@ def test_merge_meeting_users_fields(self) -> None: }, "meeting/1": { "personal_note_ids": [1], - "meeting_user_ids": [14], + "meeting_user_ids": [1, 14], }, } ) @@ -2253,7 +2285,7 @@ def test_import_amendment_paragraphs(self) -> None: "1": "<it>test</it>", "2": "broken", }, - "sequential_number": 2, + "sequential_number": 1, }, ) @@ -2263,7 +2295,7 @@ def test_import_with_wrong_decimal(self) -> None: response = self.request("meeting.import", data) self.assert_status_code(response, 400) assert ( - "user/1/default_vote_weight: Type error: Type is not None: data = self.create_request_data() meeting_data = data["meeting"] del meeting_data["meeting"]["1"]["meeting_user_ids"] + del meeting_data["meeting"]["1"]["user_ids"] del meeting_data["group"]["1"]["meeting_user_ids"] del meeting_data["user"] del meeting_data["meeting_user"] @@ -2549,8 +2582,8 @@ def test_without_users(self) -> None: ) self.assert_model_not_exists("user/2") - def test_delete_statutes(self) -> None: - """test for deleted statute motions in event.data after migration. Uses migrations 0055 and onwards.""" + def test_old_migration_index(self) -> None: + """test what happens if an old migration index is sent.""" data = self.create_request_data() data["meeting"]["meeting"]["1"][ "motions_default_statute_amendment_workflow_id" @@ -2567,32 +2600,14 @@ def test_delete_statutes(self) -> None: data["meeting"]["_migration_index"] = 55 self.replace_migrated_projector_fields(data) response = self.request("meeting.import", data) - self.assert_status_code(response, 200) - self.assert_model_not_exists("motion_workflow/2") - self.assert_model_exists( - "meeting/1", - { - "motions_default_statute_amendment_workflow_id": None, - "motions_statute_recommendations_by": None, - "motions_statutes_enabled": None, - "motion_statute_paragraph_ids": None, - }, - ) - self.assert_model_exists( - "motion_workflow/1", - { - "default_statute_amendment_workflow_meeting_id": None, - "sequential_number": 1, - }, + self.assert_status_code(response, 400) + assert ( + "Your data migration index '55' is lower than the migration index of this backend" + in response.json["message"] ) + assert "Please, use a more recent file!" in response.json["message"] - @pytest.mark.skip() - def test_import_os3_data(self) -> None: - data_raw = get_initial_data_file("data/export-OS3-demo.json") - data = {"committee_id": 1, "meeting": data_raw} - response = self.request("meeting.import", data) - self.assert_status_code(response, 200) - + # TODO: Fix this test def test_import_export_with_orga_mediafiles(self) -> None: self.create_meeting() self.set_user_groups(1, [1]) @@ -2763,7 +2778,7 @@ def test_import_export_with_orga_mediafiles(self) -> None: "show_state_extension_field": False, "merge_amendment_into_final": "undefined", "show_recommendation_extension_field": False, - "motion_ids": [3], + "motion_ids": [1, 3], }, "projector/1": { "sequential_number": 1, @@ -2825,8 +2840,8 @@ def test_import_export_with_orga_mediafiles(self) -> None: "attachment_ids": ["motion/3", "topic/4", "assignment/5"], "used_as_logo_projector_main_in_meeting_id": 1, }, - "list_of_speakers/1": { - "sequential_number": 1, + "list_of_speakers/2": { + "sequential_number": 2, "content_object_id": "meeting_mediafile/20", "speaker_ids": [6], "structure_level_list_of_speakers_ids": [7], @@ -2834,17 +2849,17 @@ def test_import_export_with_orga_mediafiles(self) -> None: "meeting_id": 1, }, "list_of_speakers/9": { - "sequential_number": 2, + "sequential_number": 3, "content_object_id": "motion/3", "meeting_id": 1, }, "list_of_speakers/11": { - "sequential_number": 3, + "sequential_number": 4, "content_object_id": "topic/4", "meeting_id": 1, }, "list_of_speakers/12": { - "sequential_number": 4, + "sequential_number": 5, "content_object_id": "assignment/5", "meeting_id": 1, }, @@ -2854,12 +2869,12 @@ def test_import_export_with_orga_mediafiles(self) -> None: "meeting_id": 1, }, "projection/8": { - "content_object_id": "list_of_speakers/1", + "content_object_id": "list_of_speakers/2", "current_projector_id": 1, "meeting_id": 1, }, "motion/3": { - "sequential_number": 1, + "sequential_number": 2, "title": "A motion", "text": "like no other", "state_id": 1, @@ -2883,7 +2898,7 @@ def test_import_export_with_orga_mediafiles(self) -> None: "meeting_id": 1, }, "speaker/6": { - "list_of_speakers_id": 1, + "list_of_speakers_id": 2, "structure_level_list_of_speakers_id": 7, "meeting_user_id": 1, "point_of_order": True, @@ -2892,7 +2907,7 @@ def test_import_export_with_orga_mediafiles(self) -> None: }, "structure_level_list_of_speakers/7": { "structure_level_id": 14, - "list_of_speakers_id": 1, + "list_of_speakers_id": 2, "speaker_ids": [6], "initial_time": 100, "remaining_time": 5, @@ -2987,15 +3002,15 @@ def test_import_export_with_orga_mediafiles(self) -> None: "committee_id": 60, "group_ids": [4, 5, 6], "projector_ids": [2], - "meeting_mediafile_ids": [], + "meeting_mediafile_ids": None, "logo_projector_main_id": None, - "list_of_speakers_ids": [13, 14, 15], - "all_projection_ids": [], - "motion_ids": [4], + "list_of_speakers_ids": [13, 14, 15, 16], + "all_projection_ids": None, + "motion_ids": [4, 5], "topic_ids": [5], "assignment_ids": [6], - "speaker_ids": [], - "structure_level_list_of_speakers_ids": [], + "speaker_ids": None, + "structure_level_list_of_speakers_ids": None, "agenda_item_ids": [11], "point_of_order_category_ids": [14], "structure_level_ids": [15], @@ -3006,27 +3021,54 @@ def test_import_export_with_orga_mediafiles(self) -> None: f"group/{id_}", { "meeting_id": 2, - "meeting_mediafile_access_group_ids": [], - "meeting_mediafile_inherited_access_group_ids": [], + "meeting_mediafile_access_group_ids": None, + "meeting_mediafile_inherited_access_group_ids": None, }, ) self.assert_model_exists( "group/6", { - "meeting_mediafile_access_group_ids": [], + "meeting_mediafile_access_group_ids": None, "meeting_mediafile_inherited_access_group_ids": None, "meeting_id": 2, }, ) self.assert_model_exists( - "meeting_user/2", {"user_id": 1, "meeting_id": 2, "speaker_ids": []} + "meeting_user/2", {"user_id": 1, "meeting_id": 2, "speaker_ids": None} + ) + self.assert_model_exists( + "motion_workflow/2", + { + "name": "blup", + "default_amendment_workflow_meeting_id": 2, + "sequential_number": 1, + }, + ) + self.assert_model_exists( + "motion_state/2", + { + "css_class": "lightblue", + "workflow_id": 2, + "name": "test", + "weight": 1, + "restrictions": [], + "allow_support": False, + "allow_create_poll": False, + "allow_submitter_edit": False, + "set_number": True, + "show_state_extension_field": False, + "merge_amendment_into_final": "undefined", + "show_recommendation_extension_field": False, + "motion_ids": [4, 5], + }, ) self.assert_model_exists( "projector/2", { "meeting_id": 2, - "current_projection_ids": [], - "history_projection_ids": [], + "used_as_reference_projector_meeting_id": 2, + "current_projection_ids": None, + "history_projection_ids": None, }, ) self.assert_model_not_exists("mediafile/6") @@ -3038,7 +3080,16 @@ def test_import_export_with_orga_mediafiles(self) -> None: "meeting_mediafile_ids": [id_ * 10], }, ) - for id_, co_id in {13: "motion/4", 14: "topic/5", 15: "assignment/6"}.items(): + self.assert_model_exists( + "list_of_speakers/13", + { + "sequential_number": 1, + "meeting_id": 2, + "content_object_id": "motion/4", + }, + ) + # list_of_speakers/2 not copied bc it belongs to a orga-related meeting_mediafile + for id_, co_id in {14: "motion/5", 15: "topic/5", 16: "assignment/6"}.items(): self.assert_model_exists( f"list_of_speakers/{id_}", { @@ -3052,11 +3103,24 @@ def test_import_export_with_orga_mediafiles(self) -> None: "motion/4", { "sequential_number": 1, + "title": "motion1", + "text": None, + "state_id": 2, + "list_of_speakers_id": 13, + "attachment_meeting_mediafile_ids": None, + "meeting_id": 2, + "number_value": 31, + }, + ) + self.assert_model_exists( + "motion/5", + { + "sequential_number": 2, "title": "A motion", "text": "like no other", "state_id": 2, - "list_of_speakers_id": 13, - "attachment_meeting_mediafile_ids": [], + "list_of_speakers_id": 14, + "attachment_meeting_mediafile_ids": None, "meeting_id": 2, }, ) @@ -3065,9 +3129,9 @@ def test_import_export_with_orga_mediafiles(self) -> None: { "title": "Stupid topic", "sequential_number": 1, - "attachment_meeting_mediafile_ids": [], + "attachment_meeting_mediafile_ids": None, "agenda_item_id": 11, - "list_of_speakers_id": 14, + "list_of_speakers_id": 15, "meeting_id": 2, }, ) @@ -3076,12 +3140,13 @@ def test_import_export_with_orga_mediafiles(self) -> None: { "title": "We're electing someone, idk", "sequential_number": 1, - "list_of_speakers_id": 15, - "attachment_meeting_mediafile_ids": [], + "list_of_speakers_id": 16, + "attachment_meeting_mediafile_ids": None, "meeting_id": 2, }, ) self.assert_model_not_exists("speaker/7") + self.assert_model_not_exists("structure_level_list_of_speakers/8") self.assert_model_exists( "agenda_item/11", {"content_object_id": "topic/5", "meeting_id": 2} ) @@ -3091,7 +3156,7 @@ def test_import_export_with_orga_mediafiles(self) -> None: "text": "Pointless point of order", "rank": 1, "meeting_id": 2, - "speaker_ids": [], + "speaker_ids": None, }, ) self.assert_model_exists( @@ -3099,6 +3164,36 @@ def test_import_export_with_orga_mediafiles(self) -> None: { "name": "Eeeeueuuurrrggghhhh", "meeting_id": 2, - "structure_level_list_of_speakers_ids": [], + "structure_level_list_of_speakers_ids": None, }, ) + + def test_without_migration_index(self) -> None: + data = self.create_request_data() + del data["meeting"]["_migration_index"] + response = self.request("meeting.import", data) + self.assert_status_code(response, 400) + self.assertIn( + "data.meeting must contain ['_migration_index'] properties", + response.json["message"], + ) + + def test_with_negative_migration_index(self) -> None: + data = self.create_request_data() + data["meeting"]["_migration_index"] = -1 + response = self.request("meeting.import", data) + self.assert_status_code(response, 400) + self.assertIn( + "data.meeting._migration_index must be bigger than or equal to 1", + response.json["message"], + ) + + def test_with_migration_index_to_high(self) -> None: + data = self.create_request_data() + data["meeting"]["_migration_index"] = self.mig_index + 1 + response = self.request("meeting.import", data) + self.assert_status_code(response, 400) + self.assertIn( + f"Your data migration index '{self.mig_index+1}' is higher than the migration index of this backend '{self.mig_index}'! Please, update your backend!", + response.json["message"], + ) diff --git a/tests/system/presenter/test_export_meeting.py b/tests/system/presenter/test_export_meeting.py index 9b13ef7f69..cadbde81fc 100644 --- a/tests/system/presenter/test_export_meeting.py +++ b/tests/system/presenter/test_export_meeting.py @@ -1,4 +1,6 @@ from datetime import datetime +from decimal import Decimal +from zoneinfo import ZoneInfo from psycopg.types.json import Jsonb @@ -16,9 +18,27 @@ def setUp(self) -> None: self.create_meeting(1, {"name": "exported_meeting"}) def test_correct(self) -> None: + self.set_models( + { + "meeting/1": { + "start_time": datetime.fromtimestamp( + 626637600, tz=ZoneInfo("Europe/Berlin") + ), + "end_time": datetime.fromtimestamp( + 654908400, tz=ZoneInfo("Europe/Berlin") + ), + } + } + ) status_code, data = self.request("export_meeting", {"meeting_id": 1}) self.assertEqual(status_code, 200) - assert data["meeting"]["1"]["name"] == "exported_meeting" + meeting = data["meeting"]["1"] + assert meeting["name"] == "exported_meeting" + # TODO: Backend is currently automatically transforming all timestamps to UTC + # When that is changed, these checks will need to be changed to something like + # "1989-11-09T19:00:00+01:00" and "1990-10-03T00:00:00+01:00" respectively + assert meeting["start_time"] == "1989-11-09T18:00:00+00:00" + assert meeting["end_time"] == "1990-10-02T23:00:00+00:00" for collection in ( "group", "projector", @@ -108,14 +128,15 @@ def test_action_worker_import_preview_exclusion(self) -> None: assert "import_preview" not in data def test_add_users(self) -> None: + self.set_user_groups(1, [1]) self.set_models( { "meeting/1": {"present_user_ids": [1]}, "gender/1": {"name": "male"}, - "user/1": {"gender_id": 1}, + "user/1": {"gender_id": 1, "default_vote_weight": "2.000000"}, + "meeting_user/1": {"vote_weight": Decimal("3.000001")}, } ) - self.set_user_groups(1, [1]) status_code, data = self.request("export_meeting", {"meeting_id": 1}) assert status_code == 200 @@ -125,8 +146,10 @@ def test_add_users(self) -> None: assert user["is_active"] is True assert user["is_present_in_meeting_ids"] == [1] assert user["gender"] == "male" + assert user["default_vote_weight"] == "2.000000" assert data["meeting_user"]["1"]["group_ids"] == [1] + assert data["meeting_user"]["1"]["vote_weight"] == "3.000001" def test_add_users_in_2_meetings(self) -> None: self.create_meeting(