diff --git a/docs/actions/motion.create_forwarded.md b/docs/actions/motion.create_forwarded.md index 5dba5949d..0b54003c7 100644 --- a/docs/actions/motion.create_forwarded.md +++ b/docs/actions/motion.create_forwarded.md @@ -12,6 +12,7 @@ use_original_submitter: boolean; use_original_number: boolean; with_amendments: boolean; + with_change_recommendations: boolean; } ``` @@ -35,6 +36,8 @@ The three boolean flags for extra rules will be applied to the amendments as wel If the forwarded amendments have amendments themselves, those will also be treated the same way +If `with_change_recommendations` is set to True, all change recommendations of the motion will be copied to the target meeting and connected to the newly forwarded lead motion. They will not have any reference to the original recommendation afterwards. + ### Forwarding tree fields * `all_origin_ids` of the newly created motion must be set to `all_origin_ids` of the origin motion plus the given `origin_id`. It is important that the id is appended at the end of the list, since the order of this field represents the order of the tree in case a motion of the tree is deleted. diff --git a/docs/actions/motion.create_forwarded_amendment.md b/docs/actions/motion.create_forwarded_amendment.md index 53338ec57..2261f41dd 100644 --- a/docs/actions/motion.create_forwarded_amendment.md +++ b/docs/actions/motion.create_forwarded_amendment.md @@ -13,6 +13,7 @@ amendment_paragraphs: JSON use_original_submitter: boolean; use_original_number: boolean; + with_change_recommendations: boolean; } ``` diff --git a/global/meta b/global/meta index ab5274633..13234af24 160000 --- a/global/meta +++ b/global/meta @@ -1 +1 @@ -Subproject commit ab527463324866435ea932305b77dc1334c1738d +Subproject commit 13234af243473a00f17fa03455efed95c18a3ddf diff --git a/openslides_backend/action/actions/motion/base_create_forwarded.py b/openslides_backend/action/actions/motion/base_create_forwarded.py index c6b31f105..2ec1cb833 100644 --- a/openslides_backend/action/actions/motion/base_create_forwarded.py +++ b/openslides_backend/action/actions/motion/base_create_forwarded.py @@ -13,6 +13,7 @@ from ....shared.interfaces.write_request import WriteRequest from ....shared.patterns import fqid_from_collection_and_id from ...util.typing import ActionData, ActionResultElement, ActionResults +from ..motion_change_recommendation.create import MotionChangeRecommendationCreateAction from .create_base import MotionCreateBase @@ -193,9 +194,31 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: self.set_origin_ids(instance) self.set_text_hash(instance) instance["forwarded"] = round(time.time()) + with_change_recommendations = instance.pop("with_change_recommendations", False) self.datastore.apply_changed_model( fqid_from_collection_and_id("motion", instance["id"]), instance ) + if with_change_recommendations: + change_recos = self.datastore.filter( + "motion_change_recommendation", + FilterOperator("motion_id", "=", instance["origin_id"]), + [ + "rejected", + "internal", + "type", + "other_description", + "line_from", + "line_to", + "text", + ], + ) + change_reco_data = [ + {**change_reco, "motion_id": instance["id"]} + for change_reco in change_recos.values() + ] + self.execute_other_action( + MotionChangeRecommendationCreateAction, change_reco_data + ) amendment_ids = self.datastore.get( fqid_from_collection_and_id("motion", instance["origin_id"]), ["amendment_ids"], @@ -254,6 +277,7 @@ def update_instance(self, instance: dict[str, Any]) -> dict[str, Any]: "meeting_id": instance["meeting_id"], "use_original_submitter": use_original_submitter, "use_original_number": use_original_number, + "with_change_recommendations": with_change_recommendations, } ) amendment.pop("meta_position", 0) diff --git a/openslides_backend/action/actions/motion/create_forwarded.py b/openslides_backend/action/actions/motion/create_forwarded.py index 23339127f..150853731 100644 --- a/openslides_backend/action/actions/motion/create_forwarded.py +++ b/openslides_backend/action/actions/motion/create_forwarded.py @@ -23,6 +23,7 @@ class MotionCreateForwarded(BaseMotionCreateForwarded): additional_optional_fields={ "use_original_submitter": {"type": "boolean"}, "use_original_number": {"type": "boolean"}, + "with_change_recommendations": {"type": "boolean"}, "with_amendments": {"type": "boolean"}, }, ) diff --git a/openslides_backend/action/actions/motion/create_forwarded_amendment.py b/openslides_backend/action/actions/motion/create_forwarded_amendment.py index 82b84d30a..85670fbf9 100644 --- a/openslides_backend/action/actions/motion/create_forwarded_amendment.py +++ b/openslides_backend/action/actions/motion/create_forwarded_amendment.py @@ -25,6 +25,7 @@ class MotionCreateForwardedAmendment(BaseMotionCreateForwarded): additional_optional_fields={ "use_original_submitter": {"type": "boolean"}, "use_original_number": {"type": "boolean"}, + "with_change_recommendations": {"type": "boolean"}, }, ) diff --git a/tests/system/action/motion/test_create_forwarded.py b/tests/system/action/motion/test_create_forwarded.py index 18784a532..3a6e05abd 100644 --- a/tests/system/action/motion/test_create_forwarded.py +++ b/tests/system/action/motion/test_create_forwarded.py @@ -1316,3 +1316,225 @@ def test_name_generation(self) -> None: "Sue B. Mid-Edit", ]: assert name in motion["additional_submitter"] + + def test_with_change_recommendations(self) -> None: + self.set_models(self.test_model) + self.set_models( + { + "motion/12": {"change_recommendation_ids": [1, 2]}, + "meeting/1": {"motion_change_recommendation_ids": [1, 2]}, + "motion_change_recommendation/1": { + "line_from": 11, + "line_to": 23, + "text": "Hello world", + "motion_id": 12, + "meeting_id": 1, + "rejected": True, + "internal": True, + "type": "replacement", + "other_description": "Iamachangerecommendation", + "creation_time": 0, + }, + "motion_change_recommendation/2": { + "line_from": 24, + "line_to": 25, + "text": "!", + "motion_id": 12, + "meeting_id": 1, + "type": "replacement", + "creation_time": 1, + }, + } + ) + response = self.request( + "motion.create_forwarded", + { + "title": "test_Xcdfgee", + "meeting_id": 2, + "origin_id": 12, + "text": "test", + "reason": "reason_jLvcgAMx", + "with_change_recommendations": True, + }, + ) + self.assert_status_code(response, 200) + created_id = response.json["results"][0][0]["id"] + self.assert_model_exists( + f"motion/{created_id}", {"change_recommendation_ids": [3, 4]} + ) + reco = self.assert_model_exists( + "motion_change_recommendation/3", + { + "line_from": 11, + "line_to": 23, + "text": "Hello world", + "motion_id": created_id, + "meeting_id": 2, + "rejected": True, + "internal": True, + "type": "replacement", + "other_description": "Iamachangerecommendation", + }, + ) + assert reco["creation_time"] > 0 + self.assert_model_exists( + "motion_change_recommendation/4", + { + "line_from": 24, + "line_to": 25, + "text": "!", + "type": "replacement", + "motion_id": created_id, + "meeting_id": 2, + }, + ) + + def test_without_change_recommendations(self) -> None: + self.set_models(self.test_model) + self.set_models( + { + "motion/12": {"change_recommendation_ids": [1, 2]}, + "meeting/1": {"motion_change_recommendation_ids": [1, 2]}, + "motion_change_recommendation/1": { + "line_from": 11, + "line_to": 23, + "text": "Hello world", + "motion_id": 12, + "meeting_id": 1, + "rejected": True, + "internal": True, + "type": "replacement", + "other_description": "Iamachangerecommendation", + "creation_time": 0, + }, + "motion_change_recommendation/2": { + "line_from": 24, + "line_to": 25, + "text": "!", + "motion_id": 12, + "meeting_id": 1, + "type": "replacement", + "creation_time": 1, + }, + } + ) + response = self.request( + "motion.create_forwarded", + { + "title": "test_Xcdfgee", + "meeting_id": 2, + "origin_id": 12, + "text": "test", + "reason": "reason_jLvcgAMx", + }, + ) + self.assert_status_code(response, 200) + created_id = response.json["results"][0][0]["id"] + self.assert_model_exists( + f"motion/{created_id}", {"change_recommendation_ids": None} + ) + self.assert_model_not_exists("motion_change_recommendation/3") + + def test_with_no_change_recommendations(self) -> None: + self.set_models(self.test_model) + response = self.request( + "motion.create_forwarded", + { + "title": "test_Xcdfgee", + "meeting_id": 2, + "origin_id": 12, + "text": "test", + "reason": "reason_jLvcgAMx", + "with_change_recommendations": True, + }, + ) + self.assert_status_code(response, 200) + + def test_with_amendment_change_recommendations(self) -> None: + self.set_models(self.test_model) + self.set_models( + { + "motion/12": {"change_recommendation_ids": [1], "amendment_ids": [13]}, + "meeting/1": {"motion_change_recommendation_ids": [1, 2]}, + "motion/13": { + "number": "AMNDMNT1", + "title": "amendment1", + "meeting_id": 1, + "derived_motion_ids": [], + "all_origin_ids": [], + "all_derived_motion_ids": [], + "lead_motion_id": 12, + "state_id": 30, + "text": "bla", + "change_recommendation_ids": [2], + }, + "motion_change_recommendation/1": { + "line_from": 11, + "line_to": 23, + "text": "Hello world", + "motion_id": 12, + "meeting_id": 1, + "rejected": True, + "internal": True, + "type": "replacement", + "other_description": "Iamachangerecommendation", + "creation_time": 0, + }, + "motion_change_recommendation/2": { + "line_from": 24, + "line_to": 25, + "text": "!", + "motion_id": 13, + "meeting_id": 1, + "type": "replacement", + "creation_time": 1, + }, + } + ) + response = self.request( + "motion.create_forwarded", + { + "title": "test_Xcdfgee", + "meeting_id": 2, + "origin_id": 12, + "text": "test", + "reason": "reason_jLvcgAMx", + "with_change_recommendations": True, + "with_amendments": True, + }, + ) + self.assert_status_code(response, 200) + created_id = response.json["results"][0][0]["id"] + self.assert_model_exists( + f"motion/{created_id}", {"change_recommendation_ids": [3]} + ) + self.assert_model_exists( + f"motion/{created_id+1}", + {"change_recommendation_ids": [4], "lead_motion_id": created_id}, + ) + reco = self.assert_model_exists( + "motion_change_recommendation/3", + { + "line_from": 11, + "line_to": 23, + "text": "Hello world", + "motion_id": created_id, + "meeting_id": 2, + "rejected": True, + "internal": True, + "type": "replacement", + "other_description": "Iamachangerecommendation", + }, + ) + assert reco["creation_time"] > 0 + self.assert_model_exists( + "motion_change_recommendation/4", + { + "line_from": 24, + "line_to": 25, + "text": "!", + "type": "replacement", + "motion_id": created_id + 1, + "meeting_id": 2, + }, + )