From 92a558db3b34ddd3a03513695da3e41811d5c2cc Mon Sep 17 00:00:00 2001 From: Mohamed Abdel Wedoud Date: Mon, 10 Jun 2024 19:03:19 +0200 Subject: [PATCH] feat(delete-apis): not allow deletion for areas, links and thermals when referenced in a bc --- antarest/core/exceptions.py | 33 ++++++++++++ antarest/study/service.py | 51 ++++++++++++++++++- antarest/study/web/study_data_blueprint.py | 1 + .../study_data_blueprint/test_thermal.py | 20 ++++++++ tests/integration/test_integration.py | 16 +++++- 5 files changed, 119 insertions(+), 2 deletions(-) diff --git a/antarest/core/exceptions.py b/antarest/core/exceptions.py index cf1b21d1a4..07b0f9a0f7 100644 --- a/antarest/core/exceptions.py +++ b/antarest/core/exceptions.py @@ -325,6 +325,39 @@ def __init__(self, is_variant: bool) -> None: super().__init__(HTTPStatus.EXPECTATION_FAILED, "Upgrade not supported for parent of variants") +class AreaDeletionNotAllowed(HTTPException): + def __init__(self, uuid: str, message: t.Optional[str] = None) -> None: + msg = f"Area {uuid} is not allowed to be deleted" + if message: + msg += f"\n{message}" + super().__init__( + HTTPStatus.FORBIDDEN, + msg, + ) + + +class LinkDeletionNotAllowed(HTTPException): + def __init__(self, uuid: str, message: t.Optional[str] = None) -> None: + msg = f"Link {uuid} is not allowed to be deleted" + if message: + msg += f"\n{message}" + super().__init__( + HTTPStatus.FORBIDDEN, + msg, + ) + + +class ClusterDeletionNotAllowed(HTTPException): + def __init__(self, uuid: str, message: t.Optional[str] = None) -> None: + msg = f"Cluster {uuid} is not allowed to be deleted" + if message: + msg += f"\n{message}" + super().__init__( + HTTPStatus.FORBIDDEN, + msg, + ) + + class UnsupportedStudyVersion(HTTPException): def __init__(self, version: str) -> None: super().__init__( diff --git a/antarest/study/service.py b/antarest/study/service.py index 0330992da2..46a1337ca2 100644 --- a/antarest/study/service.py +++ b/antarest/study/service.py @@ -20,9 +20,12 @@ from antarest.core.config import Config from antarest.core.exceptions import ( + AreaDeletionNotAllowed, BadEditInstructionException, + ClusterDeletionNotAllowed, CommandApplicationError, IncorrectPathError, + LinkDeletionNotAllowed, NotAManagedStudyException, StudyDeletionNotAllowed, StudyNotFoundError, @@ -56,7 +59,7 @@ from antarest.study.business.areas.renewable_management import RenewableManager from antarest.study.business.areas.st_storage_management import STStorageManager from antarest.study.business.areas.thermal_management import ThermalManager -from antarest.study.business.binding_constraint_management import BindingConstraintManager +from antarest.study.business.binding_constraint_management import BindingConstraintManager, ConstraintFilters, LinkTerm from antarest.study.business.config_management import ConfigManager from antarest.study.business.correlation_management import CorrelationManager from antarest.study.business.district_manager import DistrictManager @@ -137,6 +140,7 @@ logger = logging.getLogger(__name__) MAX_MISSING_STUDY_TIMEOUT = 2 # days +MAX_BINDING_CONSTRAINTS_TO_DISPLAY = 10 def get_disk_usage(path: t.Union[str, Path]) -> int: @@ -1858,6 +1862,16 @@ def delete_area(self, uuid: str, area_id: str, params: RequestParameters) -> Non study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) + referencing_binding_constraints = self.binding_constraint_manager.get_binding_constraints( + study, ConstraintFilters(area_name=area_id) + )[:MAX_BINDING_CONSTRAINTS_TO_DISPLAY] + if referencing_binding_constraints: + first_bcs_ids = "" + for i, bc in enumerate(referencing_binding_constraints): + first_bcs_ids += f"{i+1}- {bc.id}\n" + raise AreaDeletionNotAllowed( + area_id, "Area is referenced in the following binding constraints:\n" + first_bcs_ids + ) self.areas.delete_area(study, area_id) self.event_bus.push( Event( @@ -1877,6 +1891,17 @@ def delete_link( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) + link_id = LinkTerm(area1=area_from, area2=area_to).generate_id() + referencing_binding_constraints = self.binding_constraint_manager.get_binding_constraints( + study, ConstraintFilters(link_id=link_id) + )[:MAX_BINDING_CONSTRAINTS_TO_DISPLAY] + if referencing_binding_constraints: + first_bcs_ids = "" + for i, bc in enumerate(referencing_binding_constraints): + first_bcs_ids += f"{i+1}- {bc.id}\n" + raise LinkDeletionNotAllowed( + link_id, "Link is referenced in the following binding constraints:\n" + first_bcs_ids + ) self.links.delete_link(study, area_from, area_to) self.event_bus.push( Event( @@ -2518,3 +2543,27 @@ def get_matrix_with_index_and_header( ) return df_matrix + + def assert_no_cluster_referenced_in_bcs(self, study: Study, cluster_ids: t.Sequence[str]) -> None: + """ + Check that no cluster is referenced in a binding constraint otherwise raise an ClusterDeletionNotAllowed Exception + + Args: + study: input study + cluster_ids: cluster IDs to be checked + + Returns: + + """ + + for cluster_id in cluster_ids: + referencing_binding_constraints = self.binding_constraint_manager.get_binding_constraints( + study, ConstraintFilters(cluster_id=cluster_id) + )[:MAX_BINDING_CONSTRAINTS_TO_DISPLAY] + if referencing_binding_constraints: + first_bcs_ids = "" + for i, bc in enumerate(referencing_binding_constraints): + first_bcs_ids += f"{i+1}- {bc.id}\n" + raise ClusterDeletionNotAllowed( + cluster_id, "Cluster is referenced in the following binding constraints:\n" + first_bcs_ids + ) diff --git a/antarest/study/web/study_data_blueprint.py b/antarest/study/web/study_data_blueprint.py index e84029ff8f..08e3d93711 100644 --- a/antarest/study/web/study_data_blueprint.py +++ b/antarest/study/web/study_data_blueprint.py @@ -2185,6 +2185,7 @@ def delete_thermal_clusters( ) request_params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, request_params) + study_service.assert_no_cluster_referenced_in_bcs(study, cluster_ids) study_service.thermal_manager.delete_clusters(study, area_id, cluster_ids) @bp.get( diff --git a/tests/integration/study_data_blueprint/test_thermal.py b/tests/integration/study_data_blueprint/test_thermal.py index e3f62eca1e..11ebf7289c 100644 --- a/tests/integration/study_data_blueprint/test_thermal.py +++ b/tests/integration/study_data_blueprint/test_thermal.py @@ -40,6 +40,7 @@ from antarest.core.utils.string import to_camel_case from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.thermal import ThermalProperties +from antarest.study.storage.variantstudy.model.command.common import CommandName from tests.integration.utils import wait_task_completion DEFAULT_PROPERTIES = json.loads(ThermalProperties(name="Dummy").json()) @@ -1003,6 +1004,25 @@ def test_variant_lifecycle(self, client: TestClient, user_access_token: str, var assert res.status_code == 200 assert res.json()["data"] == matrix + # # create a binding constraint for + # res = client.post( + # f"/v1/studies/{variant_id}/commands", + # headers={"Authorization": f"Bearer {user_access_token}"}, + # json=[ + # { + # "action": CommandName.CREATE_BINDING_CONSTRAINT.value, + # "args": { + # "name": "binding constraint 1", + # "enabled": True, + # "time_step": "hourly", + # "operator": "less", + # "coeffs": {f"{area_id}.{cluster_id}": [2.0, 4]}, + # }, + # } + # ], + # ) + # res.raise_for_status() + # Delete the thermal cluster res = client.delete( f"/v1/studies/{variant_id}/areas/{area_id}/clusters/thermal", diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 55e182c7d3..18d0750cd7 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -1427,8 +1427,22 @@ def test_area_management(client: TestClient, admin_access_token: str) -> None: }, } + # check that at this stage the area cannot be deleted as it is referenced in binding constraint 1 result = client.delete(f"/v1/studies/{study_id}/areas/area%201") - assert result.status_code == 200 + assert result.status_code == 403, res.json() + # verify the error message + assert ( + result.json()["description"] == "Area area 1 is not allowed to be deleted\nArea is referenced " + "in the following binding constraints:\n1- binding constraint 1\n" + ) + # check the exception + assert result.json()["exception"] == "AreaDeletionNotAllowed" + + # delete binding constraint 1 + result = client.delete(f"/v1/studies/{study_id}/bindingconstraints/binding%20constraint%201") + # check now that we can delete the area 1 + result = client.delete(f"/v1/studies/{study_id}/areas/area%201") + assert result.status_code == 200, res.json() res_areas = client.get(f"/v1/studies/{study_id}/areas") assert res_areas.json() == [ {