diff --git a/antarest/__init__.py b/antarest/__init__.py
index 8a0831bff2..f04a15b16f 100644
--- a/antarest/__init__.py
+++ b/antarest/__init__.py
@@ -7,9 +7,9 @@
# Standard project metadata
-__version__ = "2.15.3"
+__version__ = "2.15.4"
__author__ = "RTE, Antares Web Team"
-__date__ = "2023-10-12"
+__date__ = "2023-10-25"
# noinspection SpellCheckingInspection
__credits__ = "(c) Réseau de Transport de l’Électricité (RTE)"
diff --git a/antarest/study/business/district_manager.py b/antarest/study/business/district_manager.py
index de95c39925..5a214c284c 100644
--- a/antarest/study/business/district_manager.py
+++ b/antarest/study/business/district_manager.py
@@ -100,7 +100,7 @@ def create_district(
output=dto.output,
comments=dto.comments,
base_filter=DistrictBaseFilter.remove_all,
- filter_items=areas,
+ filter_items=list(areas),
command_context=self.storage_service.variant_study_service.command_factory.command_context,
)
execute_or_add_commands(study, file_study, [command], self.storage_service)
diff --git a/antarest/study/service.py b/antarest/study/service.py
index 64d5c7d83c..5bf67200b7 100644
--- a/antarest/study/service.py
+++ b/antarest/study/service.py
@@ -1397,7 +1397,6 @@ def _create_edit_study_command(
elif isinstance(tree_node, RawFileNode):
if url.split("/")[-1] == "comments":
return UpdateComments(
- target=url,
comments=data,
command_context=context,
)
diff --git a/antarest/study/storage/variantstudy/business/command_extractor.py b/antarest/study/storage/variantstudy/business/command_extractor.py
index ab1f8b6d42..83b88de09a 100644
--- a/antarest/study/storage/variantstudy/business/command_extractor.py
+++ b/antarest/study/storage/variantstudy/business/command_extractor.py
@@ -313,14 +313,12 @@ def extract_district(self, study: FileStudy, district_id: str) -> List[ICommand]
study_config = study.config
study_tree = study.tree
district_config = study_config.sets[district_id]
+ base_filter = DistrictBaseFilter.add_all if district_config.inverted_set else DistrictBaseFilter.remove_all
district_fetched_config = study_tree.get(["input", "areas", "sets", district_id])
study_commands.append(
CreateDistrict(
name=district_config.name,
- metadata={},
- base_filter=DistrictBaseFilter.add_all
- if district_config.inverted_set
- else DistrictBaseFilter.remove_all,
+ base_filter=base_filter,
filter_items=district_config.areas or [],
output=district_config.output,
comments=district_fetched_config.get("comments", None),
@@ -331,9 +329,11 @@ def extract_district(self, study: FileStudy, district_id: str) -> List[ICommand]
def extract_comments(self, study: FileStudy) -> List[ICommand]:
study_tree = study.tree
+ content = cast(bytes, study_tree.get(["settings", "comments"]))
+ comments = content.decode("utf-8")
return [
UpdateComments(
- comments=study_tree.get(["settings", "comments"]),
+ comments=comments,
command_context=self.command_context,
)
]
@@ -392,8 +392,8 @@ def generate_update_comments(
self,
study_tree: FileStudyTree,
) -> ICommand:
- url = ["settings", "comments"]
- comments = study_tree.get(url)
+ content = cast(bytes, study_tree.get(["settings", "comments"]))
+ comments = content.decode("utf-8")
return UpdateComments(
comments=comments,
command_context=self.command_context,
@@ -444,8 +444,7 @@ def generate_update_district(
district_config = study_config.sets[district_id]
district_fetched_config = study_tree.get(["input", "areas", "sets", district_id])
return UpdateDistrict(
- name=district_config.name,
- metadata={},
+ id=district_config.name,
base_filter=DistrictBaseFilter.add_all if district_config.inverted_set else DistrictBaseFilter.remove_all,
filter_items=district_config.areas or [],
output=district_config.output,
diff --git a/antarest/study/storage/variantstudy/model/command/create_cluster.py b/antarest/study/storage/variantstudy/model/command/create_cluster.py
index 0c0b58ace3..8097cad542 100644
--- a/antarest/study/storage/variantstudy/model/command/create_cluster.py
+++ b/antarest/study/storage/variantstudy/model/command/create_cluster.py
@@ -18,20 +18,30 @@
class CreateCluster(ICommand):
+ """
+ Command used to create a thermal cluster in an area.
+ """
+
+ # Overloaded metadata
+ # ===================
+
+ command_name = CommandName.CREATE_THERMAL_CLUSTER
+ version = 1
+
+ # Command parameters
+ # ==================
+
area_id: str
cluster_name: str
parameters: Dict[str, str]
prepro: Optional[Union[List[List[MatrixData]], str]] = None
modulation: Optional[Union[List[List[MatrixData]], str]] = None
- def __init__(self, **data: Any) -> None:
- super().__init__(command_name=CommandName.CREATE_THERMAL_CLUSTER, version=1, **data)
-
@validator("cluster_name")
def validate_cluster_name(cls, val: str) -> str:
valid_name = transform_name_to_id(val, lower=False)
if valid_name != val:
- raise ValueError("Area name must only contains [a-zA-Z0-9],&,-,_,(,) characters")
+ raise ValueError("Cluster name must only contains [a-zA-Z0-9],&,-,_,(,) characters")
return val
@validator("prepro", always=True)
@@ -57,13 +67,14 @@ def validate_modulation(
return validate_matrix(v, values)
def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, Dict[str, Any]]:
+ # Search the Area in the configuration
if self.area_id not in study_data.areas:
return (
CommandOutput(
status=False,
- message=f"Area '{self.area_id}' does not exist",
+ message=f"Area '{self.area_id}' does not exist in the study configuration.",
),
- dict(),
+ {},
)
cluster_id = transform_name_to_id(self.cluster_name)
for cluster in study_data.areas[self.area_id].thermals:
@@ -71,15 +82,15 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput,
return (
CommandOutput(
status=False,
- message=f"Cluster '{self.cluster_name}' already exist",
+ message=f"Thermal cluster '{cluster_id}' already exists in the area '{self.area_id}'.",
),
- dict(),
+ {},
)
study_data.areas[self.area_id].thermals.append(Cluster(id=cluster_id, name=self.cluster_name))
return (
CommandOutput(
status=True,
- message=f"Cluster '{self.cluster_name}' added to area '{self.area_id}'",
+ message=f"Thermal cluster '{cluster_id}' added to area '{self.area_id}'.",
),
{"cluster_id": cluster_id},
)
@@ -123,7 +134,7 @@ def _apply(self, study_data: FileStudy) -> CommandOutput:
def to_dto(self) -> CommandDTO:
return CommandDTO(
- action=CommandName.CREATE_THERMAL_CLUSTER.value,
+ action=self.command_name.value,
args={
"area_id": self.area_id,
"cluster_name": self.cluster_name,
diff --git a/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py b/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py
index 66964dd68d..59502439e6 100644
--- a/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py
+++ b/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py
@@ -16,17 +16,23 @@
class CreateRenewablesCluster(ICommand):
+ """
+ Command used to create a renewable cluster in an area.
+ """
+
+ # Overloaded metadata
+ # ===================
+
+ command_name = CommandName.CREATE_RENEWABLES_CLUSTER
+ version = 1
+
+ # Command parameters
+ # ==================
+
area_id: str
cluster_name: str
parameters: Dict[str, str]
- def __init__(self, **data: Any) -> None:
- super().__init__(
- command_name=CommandName.CREATE_RENEWABLES_CLUSTER,
- version=1,
- **data,
- )
-
@validator("cluster_name")
def validate_cluster_name(cls, val: str) -> str:
valid_name = transform_name_to_id(val, lower=False)
@@ -43,20 +49,33 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput,
)
return CommandOutput(status=False, message=message), {}
+ # Search the Area in the configuration
if self.area_id not in study_data.areas:
- message = f"Area '{self.area_id}' does not exist"
- return CommandOutput(status=False, message=message), {}
+ return (
+ CommandOutput(
+ status=False,
+ message=f"Area '{self.area_id}' does not exist in the study configuration.",
+ ),
+ {},
+ )
cluster_id = transform_name_to_id(self.cluster_name)
for cluster in study_data.areas[self.area_id].renewables:
if cluster.id == cluster_id:
- message = f"Renewable cluster '{self.cluster_name}' already exist"
- return CommandOutput(status=False, message=message), {}
+ return (
+ CommandOutput(
+ status=False,
+ message=f"Renewable cluster '{cluster_id}' already exists in the area '{self.area_id}'.",
+ ),
+ {},
+ )
study_data.areas[self.area_id].renewables.append(Cluster(id=cluster_id, name=self.cluster_name))
- message = f"Renewable cluster '{self.cluster_name}' added to area '{self.area_id}'"
return (
- CommandOutput(status=True, message=message),
+ CommandOutput(
+ status=True,
+ message=f"Renewable cluster '{cluster_id}' added to area '{self.area_id}'.",
+ ),
{"cluster_id": cluster_id},
)
@@ -94,7 +113,7 @@ def _apply(self, study_data: FileStudy) -> CommandOutput:
def to_dto(self) -> CommandDTO:
return CommandDTO(
- action=CommandName.CREATE_RENEWABLES_CLUSTER.value,
+ action=self.command_name.value,
args={
"area_id": self.area_id,
"cluster_name": self.cluster_name,
diff --git a/antarest/study/web/raw_studies_blueprint.py b/antarest/study/web/raw_studies_blueprint.py
index 21d19620e2..edf09aff37 100644
--- a/antarest/study/web/raw_studies_blueprint.py
+++ b/antarest/study/web/raw_studies_blueprint.py
@@ -96,12 +96,11 @@ def get_study(
extra={"user": current_user.id},
)
parameters = RequestParameters(user=current_user)
-
- resource_path = pathlib.PurePosixPath(path)
- output = study_service.get(uuid, str(resource_path), depth=depth, formatted=formatted, params=parameters)
+ output = study_service.get(uuid, path, depth=depth, formatted=formatted, params=parameters)
if isinstance(output, bytes):
# Guess the suffix form the target data
+ resource_path = pathlib.PurePosixPath(path)
parent_cfg = study_service.get(uuid, str(resource_path.parent), depth=2, formatted=True, params=parameters)
child = parent_cfg[resource_path.name]
suffix = pathlib.PurePosixPath(child).suffix
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 1df1b0b313..3e6b6a3df2 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -1,6 +1,28 @@
Antares Web Changelog
=====================
+v2.15.4 (2023-10-25)
+--------------------
+
+### Tests
+
+* **commands:** refactored study variant command unit tests, improved coverage, and fixed deprecated attribute usage ([8bd0bdf](https://github.com/AntaresSimulatorTeam/AntaREST/commit/8bd0bdf93c1a9ef0ee12570cb7d398ba7212b2fe))
+
+
+### Bug Fixes
+
+* **results-ui:** display results for a specific year [`#1779`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1779)
+* **ui-study:** remove popup to prevent close after variant creation [`#1773`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1773)
+* **raw:** fix HTTP exception when going on debug view [`#1769`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1769)
+
+
+### Contributors
+
+laurent-laporte-pro,
+MartinBelthle
+
+
+
v2.15.3 (2023-10-12)
--------------------
diff --git a/scripts/update_version.py b/scripts/update_version.py
index f2d5e32512..a829035ff0 100755
--- a/scripts/update_version.py
+++ b/scripts/update_version.py
@@ -148,7 +148,7 @@ def upgrade_version(new_version: str, new_date: str) -> None:
if fullpath.is_file():
print(f"- updating '{fullpath.relative_to(PROJECT_DIR)}'...")
text = fullpath.read_text(encoding="utf-8")
- patched = re.sub(search, replace, text, count=1)
+ patched = re.sub(search, replace, text, count=2)
fullpath.write_text(patched, encoding="utf-8")
# Patching release date
diff --git a/setup.py b/setup.py
index 1fc961fedf..2e504d356a 100644
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@
setup(
name="AntaREST",
- version="2.15.3",
+ version="2.15.4",
description="Antares Server",
long_description=Path("README.md").read_text(encoding="utf-8"),
long_description_content_type="text/markdown",
diff --git a/sonar-project.properties b/sonar-project.properties
index 8742aa5a83..b3c424acb2 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -6,5 +6,5 @@ sonar.exclusions=antarest/gui.py,antarest/main.py
sonar.python.coverage.reportPaths=coverage.xml
sonar.python.version=3.8
sonar.javascript.lcov.reportPaths=webapp/coverage/lcov.info
-sonar.projectVersion=2.15.3
+sonar.projectVersion=2.15.4
sonar.coverage.exclusions=antarest/gui.py,antarest/main.py,antarest/singleton_services.py,antarest/worker/archive_worker_service.py,webapp/**/*
\ No newline at end of file
diff --git a/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py b/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py
index 04a6b3fbfc..3d0177b1f2 100644
--- a/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py
+++ b/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py
@@ -1,4 +1,5 @@
import http
+import itertools
import json
import pathlib
import shutil
@@ -167,3 +168,12 @@ def test_get_study(
)
assert res.status_code == 200
assert np.isnan(res.json()["data"][0]).any()
+
+ # Iterate over all possible combinations of path and depth
+ for path, depth in itertools.product([None, "", "/"], [0, 1, 2]):
+ res = client.get(
+ f"/v1/studies/{study_id}/raw",
+ params={"path": path, "depth": depth},
+ headers=headers,
+ )
+ assert res.status_code == 200, f"Error for path={path} and depth={depth}"
diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py
index df4249ca47..dd1d2b3179 100644
--- a/tests/integration/test_integration.py
+++ b/tests/integration/test_integration.py
@@ -6,7 +6,6 @@
from starlette.testclient import TestClient
from antarest.core.model import PublicMode
-from antarest.core.tasks.model import TaskDTO, TaskStatus
from antarest.study.business.adequacy_patch_management import PriceTakingOrder
from antarest.study.business.area_management import AreaType, LayerInfoDTO
from antarest.study.business.areas.properties_management import AdequacyPatchMode
@@ -104,17 +103,19 @@ def test_main(client: TestClient, admin_access_token: str, study_id: str) -> Non
assert res.json()["description"] == "Not a year by year simulation"
# Set new comments
- client.put(
+ res = client.put(
f"/v1/studies/{study_id}/comments",
headers={"Authorization": f'Bearer {george_credentials["access_token"]}'},
json={"comments": comments},
)
+ assert res.status_code == 204, res.json()
# Get comments
res = client.get(
f"/v1/studies/{study_id}/comments",
headers={"Authorization": f'Bearer {george_credentials["access_token"]}'},
)
+ assert res.status_code == 200, res.json()
assert res.json() == comments
# study synthesis
@@ -122,7 +123,7 @@ def test_main(client: TestClient, admin_access_token: str, study_id: str) -> Non
f"/v1/studies/{study_id}/synthesis",
headers={"Authorization": f'Bearer {george_credentials["access_token"]}'},
)
- assert res.status_code == 200
+ assert res.status_code == 200, res.json()
# playlist
res = client.post(
@@ -1977,188 +1978,6 @@ def test_archive(client: TestClient, admin_access_token: str, study_id: str, tmp
assert not (tmp_path / "archive_dir" / f"{study_id}.zip").exists()
-def test_variant_manager(client: TestClient, admin_access_token: str, study_id: str) -> None:
- admin_headers = {"Authorization": f"Bearer {admin_access_token}"}
-
- base_study_res = client.post("/v1/studies?name=foo", headers=admin_headers)
-
- base_study_id = base_study_res.json()
-
- res = client.post(f"/v1/studies/{base_study_id}/variants?name=foo", headers=admin_headers)
- variant_id = res.json()
-
- client.post(f"/v1/launcher/run/{variant_id}", headers=admin_headers)
-
- res = client.get(f"v1/studies/{variant_id}/synthesis", headers=admin_headers)
-
- assert variant_id in res.json()["output_path"]
-
- client.post(f"/v1/studies/{variant_id}/variants?name=bar", headers=admin_headers)
- client.post(f"/v1/studies/{variant_id}/variants?name=baz", headers=admin_headers)
- res = client.get(f"/v1/studies/{base_study_id}/variants", headers=admin_headers)
- children = res.json()
- assert children["node"]["name"] == "foo"
- assert len(children["children"]) == 1
- assert children["children"][0]["node"]["name"] == "foo"
- assert len(children["children"][0]["children"]) == 2
- assert children["children"][0]["children"][0]["node"]["name"] == "bar"
- assert children["children"][0]["children"][1]["node"]["name"] == "baz"
-
- # George creates a base study
- # He creates a variant from this study : assert that no command is created
- # The admin creates a variant from the same base study : assert that its author is admin (created via a command)
-
- client.post(
- "/v1/users",
- headers=admin_headers,
- json={"name": "George", "password": "mypass"},
- )
- res = client.post("/v1/login", json={"username": "George", "password": "mypass"})
- george_credentials = res.json()
- base_study_res = client.post(
- "/v1/studies?name=foo",
- headers={"Authorization": f'Bearer {george_credentials["access_token"]}'},
- )
-
- base_study_id = base_study_res.json()
- res = client.post(
- f"/v1/studies/{base_study_id}/variants?name=foo_2",
- headers={"Authorization": f'Bearer {george_credentials["access_token"]}'},
- )
- variant_id = res.json()
- res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers)
- assert len(res.json()) == 0
- res = client.post(f"/v1/studies/{base_study_id}/variants?name=foo", headers=admin_headers)
- variant_id = res.json()
- res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers)
- assert len(res.json()) == 1
- command = res.json()[0]
- assert command["action"] == "update_config"
- assert command["args"]["target"] == "study"
- assert command["args"]["data"]["antares"]["author"] == "admin"
-
- res = client.get(f"/v1/studies/{variant_id}/parents", headers=admin_headers)
- assert len(res.json()) == 1
- assert res.json()[0]["id"] == base_study_id
- assert res.status_code == 200
-
- res = client.post(
- f"/v1/studies/{variant_id}/commands",
- json=[
- {
- "action": "create_area",
- "args": {"area_name": "testZone", "metadata": {}},
- }
- ],
- headers=admin_headers,
- )
- assert res.status_code == 200
- assert len(res.json()) == 1
-
- res = client.post(
- f"/v1/studies/{variant_id}/commands",
- json=[
- {
- "action": "create_area",
- "args": {"area_name": "testZone2", "metadata": {}},
- }
- ],
- headers=admin_headers,
- )
- assert res.status_code == 200
-
- res = client.post(
- f"/v1/studies/{variant_id}/command",
- json={
- "action": "create_area",
- "args": {"area_name": "testZone3", "metadata": {}},
- },
- headers=admin_headers,
- )
- assert res.status_code == 200
-
- command_id = res.json()
- res = client.put(
- f"/v1/studies/{variant_id}/commands/{command_id}",
- json={
- "action": "create_area",
- "args": {"area_name": "testZone4", "metadata": {}},
- },
- headers=admin_headers,
- )
- assert res.status_code == 200
-
- res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers)
- assert len(res.json()) == 4
- assert res.status_code == 200
-
- res = client.put(
- f"/v1/studies/{variant_id}/commands",
- json=[
- {
- "action": "create_area",
- "args": {"area_name": "testZoneReplace1", "metadata": {}},
- },
- {
- "action": "create_area",
- "args": {"area_name": "testZoneReplace1", "metadata": {}},
- },
- ],
- headers=admin_headers,
- )
- assert res.status_code == 200
-
- res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers)
- assert len(res.json()) == 2
- assert res.status_code == 200
-
- command_id = res.json()[1]["id"]
-
- res = client.put(f"/v1/studies/{variant_id}/commands/{command_id}/move?index=0", headers=admin_headers)
- assert res.status_code == 200
-
- res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers)
- assert res.json()[0]["id"] == command_id
- assert res.status_code == 200
-
- res = client.delete(f"/v1/studies/{variant_id}/commands/{command_id}", headers=admin_headers)
-
- assert res.status_code == 200
-
- res = client.put(f"/v1/studies/{variant_id}/generate", headers=admin_headers)
- assert res.status_code == 200
-
- res = client.get(f"/v1/tasks/{res.json()}?wait_for_completion=true", headers=admin_headers)
- assert res.status_code == 200
- task_result = TaskDTO.parse_obj(res.json())
- assert task_result.status == TaskStatus.COMPLETED
- assert task_result.result.success # type: ignore
-
- res = client.get(f"/v1/studies/{variant_id}", headers=admin_headers)
- assert res.status_code == 200
-
- res = client.post(f"/v1/studies/{variant_id}/freeze?name=bar", headers=admin_headers)
- assert res.status_code == 500
-
- new_study_id = "newid"
-
- res = client.get(f"/v1/studies/{new_study_id}", headers=admin_headers)
- assert res.status_code == 404
-
- res = client.delete(f"/v1/studies/{variant_id}/commands", headers=admin_headers)
- assert res.status_code == 200
-
- res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers)
- assert res.status_code == 200
- assert len(res.json()) == 0
-
- res = client.delete(f"/v1/studies/{variant_id}", headers=admin_headers)
- assert res.status_code == 200
-
- res = client.get(f"/v1/studies/{variant_id}", headers=admin_headers)
- assert res.status_code == 404
-
-
def test_maintenance(client: TestClient, admin_access_token: str, study_id: str) -> None:
admin_headers = {"Authorization": f"Bearer {admin_access_token}"}
diff --git a/tests/integration/variant_blueprint/test_variant_manager.py b/tests/integration/variant_blueprint/test_variant_manager.py
new file mode 100644
index 0000000000..5af256dbbe
--- /dev/null
+++ b/tests/integration/variant_blueprint/test_variant_manager.py
@@ -0,0 +1,188 @@
+import logging
+
+from starlette.testclient import TestClient
+
+from antarest.core.tasks.model import TaskDTO, TaskStatus
+
+
+def test_variant_manager(client: TestClient, admin_access_token: str, study_id: str, caplog) -> None:
+ with caplog.at_level(level=logging.WARNING):
+ admin_headers = {"Authorization": f"Bearer {admin_access_token}"}
+
+ base_study_res = client.post("/v1/studies?name=foo", headers=admin_headers)
+
+ base_study_id = base_study_res.json()
+
+ res = client.post(f"/v1/studies/{base_study_id}/variants?name=foo", headers=admin_headers)
+ variant_id = res.json()
+
+ client.post(f"/v1/launcher/run/{variant_id}", headers=admin_headers)
+
+ res = client.get(f"v1/studies/{variant_id}/synthesis", headers=admin_headers)
+
+ assert variant_id in res.json()["output_path"]
+
+ client.post(f"/v1/studies/{variant_id}/variants?name=bar", headers=admin_headers)
+ client.post(f"/v1/studies/{variant_id}/variants?name=baz", headers=admin_headers)
+ res = client.get(f"/v1/studies/{base_study_id}/variants", headers=admin_headers)
+ children = res.json()
+ assert children["node"]["name"] == "foo"
+ assert len(children["children"]) == 1
+ assert children["children"][0]["node"]["name"] == "foo"
+ assert len(children["children"][0]["children"]) == 2
+ assert children["children"][0]["children"][0]["node"]["name"] == "bar"
+ assert children["children"][0]["children"][1]["node"]["name"] == "baz"
+
+ # George creates a base study
+ # He creates a variant from this study : assert that no command is created
+ # The admin creates a variant from the same base study : assert that its author is admin (created via a command)
+
+ client.post(
+ "/v1/users",
+ headers=admin_headers,
+ json={"name": "George", "password": "mypass"},
+ )
+ res = client.post("/v1/login", json={"username": "George", "password": "mypass"})
+ george_credentials = res.json()
+ base_study_res = client.post(
+ "/v1/studies?name=foo",
+ headers={"Authorization": f'Bearer {george_credentials["access_token"]}'},
+ )
+
+ base_study_id = base_study_res.json()
+ res = client.post(
+ f"/v1/studies/{base_study_id}/variants?name=foo_2",
+ headers={"Authorization": f'Bearer {george_credentials["access_token"]}'},
+ )
+ variant_id = res.json()
+ res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers)
+ assert len(res.json()) == 0
+ res = client.post(f"/v1/studies/{base_study_id}/variants?name=foo", headers=admin_headers)
+ variant_id = res.json()
+ res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers)
+ assert len(res.json()) == 1
+ command = res.json()[0]
+ assert command["action"] == "update_config"
+ assert command["args"]["target"] == "study"
+ assert command["args"]["data"]["antares"]["author"] == "admin"
+
+ res = client.get(f"/v1/studies/{variant_id}/parents", headers=admin_headers)
+ assert len(res.json()) == 1
+ assert res.json()[0]["id"] == base_study_id
+ assert res.status_code == 200
+
+ res = client.post(
+ f"/v1/studies/{variant_id}/commands",
+ json=[
+ {
+ "action": "create_area",
+ "args": {"area_name": "testZone", "metadata": {}},
+ }
+ ],
+ headers=admin_headers,
+ )
+ assert res.status_code == 200
+ assert len(res.json()) == 1
+
+ res = client.post(
+ f"/v1/studies/{variant_id}/commands",
+ json=[
+ {
+ "action": "create_area",
+ "args": {"area_name": "testZone2", "metadata": {}},
+ }
+ ],
+ headers=admin_headers,
+ )
+ assert res.status_code == 200
+
+ res = client.post(
+ f"/v1/studies/{variant_id}/command",
+ json={
+ "action": "create_area",
+ "args": {"area_name": "testZone3", "metadata": {}},
+ },
+ headers=admin_headers,
+ )
+ assert res.status_code == 200
+
+ command_id = res.json()
+ res = client.put(
+ f"/v1/studies/{variant_id}/commands/{command_id}",
+ json={
+ "action": "create_area",
+ "args": {"area_name": "testZone4", "metadata": {}},
+ },
+ headers=admin_headers,
+ )
+ assert res.status_code == 200
+
+ res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers)
+ assert len(res.json()) == 4
+ assert res.status_code == 200
+
+ res = client.put(
+ f"/v1/studies/{variant_id}/commands",
+ json=[
+ {
+ "action": "create_area",
+ "args": {"area_name": "testZoneReplace1", "metadata": {}},
+ },
+ {
+ "action": "create_area",
+ "args": {"area_name": "testZoneReplace1", "metadata": {}},
+ },
+ ],
+ headers=admin_headers,
+ )
+ assert res.status_code == 200
+
+ res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers)
+ assert len(res.json()) == 2
+ assert res.status_code == 200
+
+ command_id = res.json()[1]["id"]
+
+ res = client.put(f"/v1/studies/{variant_id}/commands/{command_id}/move?index=0", headers=admin_headers)
+ assert res.status_code == 200
+
+ res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers)
+ assert res.json()[0]["id"] == command_id
+ assert res.status_code == 200
+
+ res = client.delete(f"/v1/studies/{variant_id}/commands/{command_id}", headers=admin_headers)
+
+ assert res.status_code == 200
+
+ res = client.put(f"/v1/studies/{variant_id}/generate", headers=admin_headers)
+ assert res.status_code == 200
+
+ res = client.get(f"/v1/tasks/{res.json()}?wait_for_completion=true", headers=admin_headers)
+ assert res.status_code == 200
+ task_result = TaskDTO.parse_obj(res.json())
+ assert task_result.status == TaskStatus.COMPLETED
+ assert task_result.result.success # type: ignore
+
+ res = client.get(f"/v1/studies/{variant_id}", headers=admin_headers)
+ assert res.status_code == 200
+
+ res = client.post(f"/v1/studies/{variant_id}/freeze?name=bar", headers=admin_headers)
+ assert res.status_code == 500
+
+ new_study_id = "newid"
+
+ res = client.get(f"/v1/studies/{new_study_id}", headers=admin_headers)
+ assert res.status_code == 404
+
+ res = client.delete(f"/v1/studies/{variant_id}/commands", headers=admin_headers)
+ assert res.status_code == 200
+
+ res = client.get(f"/v1/studies/{variant_id}/commands", headers=admin_headers)
+ assert res.status_code == 200
+ assert len(res.json()) == 0
+
+ res = client.delete(f"/v1/studies/{variant_id}", headers=admin_headers)
+ assert res.status_code == 200
+
+ res = client.get(f"/v1/studies/{variant_id}", headers=admin_headers)
+ assert res.status_code == 404
diff --git a/tests/variantstudy/conftest.py b/tests/variantstudy/conftest.py
index 526d5b53ba..9db21ab220 100644
--- a/tests/variantstudy/conftest.py
+++ b/tests/variantstudy/conftest.py
@@ -1,11 +1,14 @@
import hashlib
+import typing as t
import zipfile
from pathlib import Path
from unittest.mock import Mock
import numpy as np
+import numpy.typing as npt
import pytest
+from antarest.matrixstore.model import MatrixDTO
from antarest.matrixstore.service import MatrixService
from antarest.matrixstore.uri_resolver_service import UriResolverService
from antarest.study.repository import StudyMetadataRepository
@@ -29,17 +32,50 @@ def matrix_service_fixture() -> MatrixService:
An instance of the `SimpleMatrixService` class representing the matrix service.
"""
- def create(data):
+ matrix_map: t.Dict[str, npt.NDArray[np.float64]] = {}
+
+ def create(data: t.Union[t.List[t.List[float]], npt.NDArray[np.float64]]) -> str:
"""
This function calculates a unique ID for each matrix, without storing
any data in the file system or the database.
"""
matrix = data if isinstance(data, np.ndarray) else np.array(data, dtype=np.float64)
matrix_hash = hashlib.sha256(matrix.data).hexdigest()
+ matrix_map[matrix_hash] = matrix
return matrix_hash
+ def get(matrix_id: str) -> MatrixDTO:
+ """
+ This function retrieves the matrix from the map.
+ """
+ data = matrix_map[matrix_id]
+ return MatrixDTO(
+ id=matrix_id,
+ width=data.shape[1],
+ height=data.shape[0],
+ index=[str(i) for i in range(data.shape[0])],
+ columns=[str(i) for i in range(data.shape[1])],
+ data=data.tolist(),
+ )
+
+ def exists(matrix_id: str) -> bool:
+ """
+ This function checks if the matrix exists in the map.
+ """
+ return matrix_id in matrix_map
+
+ def delete(matrix_id: str) -> None:
+ """
+ This function deletes the matrix from the map.
+ """
+ del matrix_map[matrix_id]
+
matrix_service = Mock(spec=MatrixService)
matrix_service.create.side_effect = create
+ matrix_service.get.side_effect = get
+ matrix_service.exists.side_effect = exists
+ matrix_service.delete.side_effect = delete
+
return matrix_service
diff --git a/tests/variantstudy/model/command/test_create_area.py b/tests/variantstudy/model/command/test_create_area.py
index 8c23a95f63..62e01aeba4 100644
--- a/tests/variantstudy/model/command/test_create_area.py
+++ b/tests/variantstudy/model/command/test_create_area.py
@@ -141,13 +141,7 @@ def test_apply(
assert output.status
- create_area_command: ICommand = CreateArea.parse_obj(
- {
- "area_name": area_name,
- "metadata": {},
- "command_context": command_context,
- }
- )
+ create_area_command: ICommand = CreateArea(area_name=area_name, command_context=command_context, metadata={})
output = create_area_command.apply(study_data=empty_study)
assert not output.status
diff --git a/tests/variantstudy/model/command/test_create_cluster.py b/tests/variantstudy/model/command/test_create_cluster.py
index 06d368a1e0..4fdeb3c488 100644
--- a/tests/variantstudy/model/command/test_create_cluster.py
+++ b/tests/variantstudy/model/command/test_create_cluster.py
@@ -1,8 +1,14 @@
import configparser
+import re
+
+import numpy as np
+import pytest
+from pydantic import ValidationError
from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id
from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy
from antarest.study.storage.variantstudy.business.command_reverter import CommandReverter
+from antarest.study.storage.variantstudy.model.command.common import CommandName
from antarest.study.storage.variantstudy.model.command.create_area import CreateArea
from antarest.study.storage.variantstudy.model.command.create_cluster import CreateCluster
from antarest.study.storage.variantstudy.model.command.remove_cluster import RemoveCluster
@@ -12,22 +18,52 @@
class TestCreateCluster:
- def test_validation(self, empty_study: FileStudy):
- pass
+ def test_init(self, command_context: CommandContext):
+ prepro = np.random.rand(365, 6).tolist()
+ modulation = np.random.rand(8760, 4).tolist()
+ cl = CreateCluster(
+ area_id="foo",
+ cluster_name="Cluster1",
+ parameters={"group": "Nuclear", "unitcount": 2, "nominalcapacity": 2400},
+ command_context=command_context,
+ prepro=prepro,
+ modulation=modulation,
+ )
+
+ # Check the command metadata
+ assert cl.command_name == CommandName.CREATE_THERMAL_CLUSTER
+ assert cl.version == 1
+ assert cl.command_context is command_context
+
+ # Check the command data
+ prepro_id = command_context.matrix_service.create(prepro)
+ modulation_id = command_context.matrix_service.create(modulation)
+ assert cl.area_id == "foo"
+ assert cl.cluster_name == "Cluster1"
+ assert cl.parameters == {"group": "Nuclear", "nominalcapacity": "2400", "unitcount": "2"}
+ assert cl.prepro == f"matrix://{prepro_id}"
+ assert cl.modulation == f"matrix://{modulation_id}"
+
+ def test_validate_cluster_name(self, command_context: CommandContext):
+ with pytest.raises(ValidationError, match="cluster_name"):
+ CreateCluster(area_id="fr", cluster_name="%", command_context=command_context, parameters={})
+
+ def test_validate_prepro(self, command_context: CommandContext):
+ cl = CreateCluster(area_id="fr", cluster_name="C1", command_context=command_context, parameters={})
+ assert cl.prepro == command_context.generator_matrix_constants.get_thermal_prepro_data()
+
+ def test_validate_modulation(self, command_context: CommandContext):
+ cl = CreateCluster(area_id="fr", cluster_name="C1", command_context=command_context, parameters={})
+ assert cl.modulation == command_context.generator_matrix_constants.get_thermal_prepro_modulation()
def test_apply(self, empty_study: FileStudy, command_context: CommandContext):
study_path = empty_study.config.study_path
- area_name = "Area"
+ area_name = "DE"
area_id = transform_name_to_id(area_name, lower=True)
- cluster_name = "cluster_name"
+ cluster_name = "Cluster-1"
cluster_id = transform_name_to_id(cluster_name, lower=True)
- CreateArea.parse_obj(
- {
- "area_name": area_name,
- "command_context": command_context,
- }
- ).apply(empty_study)
+ CreateArea(area_name=area_name, command_context=command_context).apply(empty_study)
parameters = {
"group": "Other",
@@ -37,19 +73,24 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext):
"market-bid-cost": "30",
}
- command = CreateCluster.parse_obj(
- {
- "area_id": area_id,
- "cluster_name": cluster_name,
- "parameters": parameters,
- "prepro": [[0]],
- "modulation": [[0]],
- "command_context": command_context,
- }
+ prepro = np.random.rand(365, 6).tolist()
+ modulation = np.random.rand(8760, 4).tolist()
+ command = CreateCluster(
+ area_id=area_id,
+ cluster_name=cluster_name,
+ parameters=parameters,
+ prepro=prepro,
+ modulation=modulation,
+ command_context=command_context,
)
output = command.apply(empty_study)
- assert output.status
+ assert output.status is True
+ assert re.match(
+ r"Thermal cluster 'cluster-1' added to area 'de'",
+ output.message,
+ flags=re.IGNORECASE,
+ )
clusters = configparser.ConfigParser()
clusters.read(study_path / "input" / "thermal" / "clusters" / area_id / "list.ini")
@@ -63,64 +104,106 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext):
assert (study_path / "input" / "thermal" / "prepro" / area_id / cluster_id / "data.txt.link").exists()
assert (study_path / "input" / "thermal" / "prepro" / area_id / cluster_id / "modulation.txt.link").exists()
- output = CreateCluster.parse_obj(
- {
- "area_id": area_id,
- "cluster_name": cluster_name,
- "parameters": parameters,
- "prepro": [[0]],
- "modulation": [[0]],
- "command_context": command_context,
- }
+ output = CreateCluster(
+ area_id=area_id,
+ cluster_name=cluster_name,
+ parameters=parameters,
+ prepro=prepro,
+ modulation=modulation,
+ command_context=command_context,
).apply(empty_study)
- assert not output.status
-
- output = CreateCluster.parse_obj(
- {
- "area_id": "non_existent_area",
- "cluster_name": cluster_name,
- "parameters": parameters,
- "prepro": [[0]],
- "modulation": [[0]],
- "command_context": command_context,
- }
+ assert output.status is False
+ assert re.match(
+ r"Thermal cluster 'cluster-1' already exists in the area 'de'",
+ output.message,
+ flags=re.IGNORECASE,
+ )
+
+ output = CreateCluster(
+ area_id="non_existent_area",
+ cluster_name=cluster_name,
+ parameters=parameters,
+ prepro=prepro,
+ modulation=modulation,
+ command_context=command_context,
).apply(empty_study)
- assert not output.status
+ assert output.status is False
+ assert re.match(
+ r"Area 'non_existent_area' does not exist",
+ output.message,
+ flags=re.IGNORECASE,
+ )
+
+ def test_to_dto(self, command_context: CommandContext):
+ prepro = np.random.rand(365, 6).tolist()
+ modulation = np.random.rand(8760, 4).tolist()
+ command = CreateCluster(
+ area_id="foo",
+ cluster_name="Cluster1",
+ parameters={"group": "Nuclear", "unitcount": 2, "nominalcapacity": 2400},
+ command_context=command_context,
+ prepro=prepro,
+ modulation=modulation,
+ )
+ prepro_id = command_context.matrix_service.create(prepro)
+ modulation_id = command_context.matrix_service.create(modulation)
+ dto = command.to_dto()
+ assert dto.dict() == {
+ "action": "create_cluster",
+ "args": {
+ "area_id": "foo",
+ "cluster_name": "Cluster1",
+ "parameters": {"group": "Nuclear", "nominalcapacity": "2400", "unitcount": "2"},
+ "prepro": prepro_id,
+ "modulation": modulation_id,
+ },
+ "id": None,
+ "version": 1,
+ }
def test_match(command_context: CommandContext):
+ prepro = np.random.rand(365, 6).tolist()
+ modulation = np.random.rand(8760, 4).tolist()
base = CreateCluster(
area_id="foo",
cluster_name="foo",
parameters={},
- prepro=[[0]],
- modulation=[[0]],
+ prepro=prepro,
+ modulation=modulation,
command_context=command_context,
)
other_match = CreateCluster(
area_id="foo",
cluster_name="foo",
parameters={},
- prepro=[[0]],
- modulation=[[0]],
+ prepro=prepro,
+ modulation=modulation,
command_context=command_context,
)
other_not_match = CreateCluster(
area_id="foo",
cluster_name="bar",
parameters={},
- prepro=[[0]],
- modulation=[[0]],
+ prepro=prepro,
+ modulation=modulation,
command_context=command_context,
)
other_other = RemoveCluster(area_id="id", cluster_id="id", command_context=command_context)
assert base.match(other_match)
assert not base.match(other_not_match)
assert not base.match(other_other)
+
+ assert base.match(other_match, equal=True)
+ assert not base.match(other_not_match, equal=True)
+ assert not base.match(other_other, equal=True)
+
assert base.match_signature() == "create_cluster%foo%foo"
+
# check the matrices links
- matrix_id = command_context.matrix_service.create([[0]])
- assert base.get_inner_matrices() == [matrix_id, matrix_id]
+ prepro_id = command_context.matrix_service.create(prepro)
+ modulation_id = command_context.matrix_service.create(modulation)
+ assert base.get_inner_matrices() == [prepro_id, modulation_id]
def test_revert(command_context: CommandContext):
@@ -128,8 +211,6 @@ def test_revert(command_context: CommandContext):
area_id="foo",
cluster_name="foo",
parameters={},
- prepro=[[0]],
- modulation=[[0]],
command_context=command_context,
)
assert CommandReverter().revert(base, [], None) == [
@@ -142,36 +223,42 @@ def test_revert(command_context: CommandContext):
def test_create_diff(command_context: CommandContext):
+ prepro_a = np.random.rand(365, 6).tolist()
+ modulation_a = np.random.rand(8760, 4).tolist()
base = CreateCluster(
area_id="foo",
cluster_name="foo",
parameters={},
- prepro="a",
- modulation="b",
+ prepro=prepro_a,
+ modulation=modulation_a,
command_context=command_context,
)
+
+ prepro_b = np.random.rand(365, 6).tolist()
+ modulation_b = np.random.rand(8760, 4).tolist()
other_match = CreateCluster(
area_id="foo",
cluster_name="foo",
- parameters={"a": "b"},
- prepro="c",
- modulation="d",
+ parameters={"nominalcapacity": "2400"},
+ prepro=prepro_b,
+ modulation=modulation_b,
command_context=command_context,
)
+
assert base.create_diff(other_match) == [
ReplaceMatrix(
target=f"input/thermal/prepro/foo/foo/data",
- matrix="c",
+ matrix=prepro_b,
command_context=command_context,
),
ReplaceMatrix(
target=f"input/thermal/prepro/foo/foo/modulation",
- matrix="d",
+ matrix=modulation_b,
command_context=command_context,
),
UpdateConfig(
target=f"input/thermal/clusters/foo/list/foo",
- data={"a": "b"},
+ data={"nominalcapacity": "2400"},
command_context=command_context,
),
]
diff --git a/tests/variantstudy/model/command/test_create_link.py b/tests/variantstudy/model/command/test_create_link.py
index fdafb2efa6..133d8d2b7d 100644
--- a/tests/variantstudy/model/command/test_create_link.py
+++ b/tests/variantstudy/model/command/test_create_link.py
@@ -1,5 +1,6 @@
import configparser
+import numpy as np
import pytest
from pydantic import ValidationError
@@ -234,14 +235,23 @@ def test_revert(command_context: CommandContext):
def test_create_diff(command_context: CommandContext):
- base = CreateLink(area1="foo", area2="bar", series="a", command_context=command_context)
+ series_a = np.random.rand(8760, 8).tolist()
+ base = CreateLink(
+ area1="foo",
+ area2="bar",
+ series=series_a,
+ command_context=command_context,
+ )
+
+ series_b = np.random.rand(8760, 8).tolist()
other_match = CreateLink(
area1="foo",
area2="bar",
parameters={"hurdles-cost": "true"},
- series="b",
+ series=series_b,
command_context=command_context,
)
+
assert base.create_diff(other_match) == [
UpdateConfig(
target=f"input/links/bar/properties/foo",
@@ -250,7 +260,7 @@ def test_create_diff(command_context: CommandContext):
),
ReplaceMatrix(
target=f"@links_series/bar/foo",
- matrix="b",
+ matrix=series_b,
command_context=command_context,
),
]
diff --git a/tests/variantstudy/model/command/test_create_renewables_cluster.py b/tests/variantstudy/model/command/test_create_renewables_cluster.py
index e47e9277e0..fc6ac91afe 100644
--- a/tests/variantstudy/model/command/test_create_renewables_cluster.py
+++ b/tests/variantstudy/model/command/test_create_renewables_cluster.py
@@ -1,8 +1,13 @@
import configparser
+import re
+
+import pytest
+from pydantic import ValidationError
from antarest.study.storage.rawstudy.model.filesystem.config.model import ENR_MODELLING, transform_name_to_id
from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy
from antarest.study.storage.variantstudy.business.command_reverter import CommandReverter
+from antarest.study.storage.variantstudy.model.command.common import CommandName
from antarest.study.storage.variantstudy.model.command.create_area import CreateArea
from antarest.study.storage.variantstudy.model.command.create_renewables_cluster import CreateRenewablesCluster
from antarest.study.storage.variantstudy.model.command.remove_renewables_cluster import RemoveRenewablesCluster
@@ -11,65 +16,115 @@
class TestCreateRenewablesCluster:
- def test_validation(self, empty_study: FileStudy):
- pass
+ def test_init(self, command_context: CommandContext):
+ cl = CreateRenewablesCluster(
+ area_id="foo",
+ cluster_name="Cluster1",
+ parameters={"group": "Solar Thermal", "unitcount": 2, "nominalcapacity": 2400},
+ command_context=command_context,
+ )
+
+ # Check the command metadata
+ assert cl.command_name == CommandName.CREATE_RENEWABLES_CLUSTER
+ assert cl.version == 1
+ assert cl.command_context is command_context
+
+ # Check the command data
+ assert cl.area_id == "foo"
+ assert cl.cluster_name == "Cluster1"
+ assert cl.parameters == {"group": "Solar Thermal", "nominalcapacity": "2400", "unitcount": "2"}
+
+ def test_validate_cluster_name(self, command_context: CommandContext):
+ with pytest.raises(ValidationError, match="cluster_name"):
+ CreateRenewablesCluster(area_id="fr", cluster_name="%", command_context=command_context, parameters={})
def test_apply(self, empty_study: FileStudy, command_context: CommandContext):
empty_study.config.enr_modelling = ENR_MODELLING.CLUSTERS.value
study_path = empty_study.config.study_path
- area_name = "Area"
+ area_name = "DE"
area_id = transform_name_to_id(area_name, lower=True)
- cluster_name = "cluster_name"
- cluster_id = transform_name_to_id(cluster_name, lower=True)
-
- CreateArea.parse_obj(
- {
- "area_name": area_name,
- "command_context": command_context,
- }
- ).apply(empty_study)
+ cluster_name = "Cluster-1"
+
+ CreateArea(area_name=area_name, command_context=command_context).apply(empty_study)
parameters = {
"name": cluster_name,
"ts-interpretation": "power-generation",
}
- command = CreateRenewablesCluster.parse_obj(
- {
- "area_id": area_id,
- "cluster_name": cluster_name,
- "parameters": parameters,
- "command_context": command_context,
- }
+ command = CreateRenewablesCluster(
+ area_id=area_id,
+ cluster_name=cluster_name,
+ parameters=parameters,
+ command_context=command_context,
)
output = command.apply(empty_study)
- assert output.status
+ assert output.status is True
+ assert re.match(
+ r"Renewable cluster 'cluster-1' added to area 'de'",
+ output.message,
+ flags=re.IGNORECASE,
+ )
clusters = configparser.ConfigParser()
clusters.read(study_path / "input" / "renewables" / "clusters" / area_id / "list.ini")
assert str(clusters[cluster_name]["name"]) == cluster_name
assert str(clusters[cluster_name]["ts-interpretation"]) == parameters["ts-interpretation"]
- output = CreateRenewablesCluster.parse_obj(
- {
- "area_id": area_id,
- "cluster_name": cluster_name,
- "parameters": parameters,
- "command_context": command_context,
- }
+ output = CreateRenewablesCluster(
+ area_id=area_id,
+ cluster_name=cluster_name,
+ parameters=parameters,
+ command_context=command_context,
).apply(empty_study)
assert not output.status
- output = CreateRenewablesCluster.parse_obj(
- {
- "area_id": "non_existent_area",
- "cluster_name": cluster_name,
- "parameters": parameters,
- "command_context": command_context,
- }
+ output = CreateRenewablesCluster(
+ area_id=area_id,
+ cluster_name=cluster_name,
+ parameters=parameters,
+ command_context=command_context,
).apply(empty_study)
- assert not output.status
+ assert output.status is False
+
+ assert re.match(
+ r"Renewable cluster 'cluster-1' already exists in the area 'de'",
+ output.message,
+ flags=re.IGNORECASE,
+ )
+
+ output = CreateRenewablesCluster(
+ area_id="non_existent_area",
+ cluster_name=cluster_name,
+ parameters=parameters,
+ command_context=command_context,
+ ).apply(empty_study)
+ assert output.status is False
+ assert re.match(
+ r"Area 'non_existent_area' does not exist",
+ output.message,
+ flags=re.IGNORECASE,
+ )
+
+ def test_to_dto(self, command_context: CommandContext):
+ command = CreateRenewablesCluster(
+ area_id="foo",
+ cluster_name="Cluster1",
+ parameters={"group": "Solar Thermal", "unitcount": 2, "nominalcapacity": 2400},
+ command_context=command_context,
+ )
+ dto = command.to_dto()
+ assert dto.dict() == {
+ "action": "create_renewables_cluster", # "renewables" with a final "s".
+ "args": {
+ "area_id": "foo",
+ "cluster_name": "Cluster1",
+ "parameters": {"group": "Solar Thermal", "nominalcapacity": "2400", "unitcount": "2"},
+ },
+ "id": None,
+ "version": 1,
+ }
def test_match(command_context: CommandContext):
@@ -95,6 +150,11 @@ def test_match(command_context: CommandContext):
assert base.match(other_match)
assert not base.match(other_not_match)
assert not base.match(other_other)
+
+ assert base.match(other_match, equal=True)
+ assert not base.match(other_not_match, equal=True)
+ assert not base.match(other_other, equal=True)
+
assert base.match_signature() == "create_renewables_cluster%foo%foo"
assert base.get_inner_matrices() == []
diff --git a/tests/variantstudy/model/command/test_manage_binding_constraints.py b/tests/variantstudy/model/command/test_manage_binding_constraints.py
index d22e05ce1e..a1309c2e47 100644
--- a/tests/variantstudy/model/command/test_manage_binding_constraints.py
+++ b/tests/variantstudy/model/command/test_manage_binding_constraints.py
@@ -1,5 +1,7 @@
from unittest.mock import Mock
+import numpy as np
+
from antarest.study.storage.rawstudy.io.reader import IniReader
from antarest.study.storage.rawstudy.model.filesystem.config.binding_constraint import BindingConstraintFrequency
from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy
@@ -337,22 +339,25 @@ def test_revert(command_context: CommandContext):
def test_create_diff(command_context: CommandContext):
+ values_a = np.random.rand(365, 3).tolist()
base = CreateBindingConstraint(
name="foo",
enabled=False,
time_step=BindingConstraintFrequency.DAILY,
operator=BindingConstraintOperator.BOTH,
coeffs={"a": [0.3]},
- values="a",
+ values=values_a,
command_context=command_context,
)
+
+ values_b = np.random.rand(8760, 3).tolist()
other_match = CreateBindingConstraint(
name="foo",
enabled=True,
time_step=BindingConstraintFrequency.HOURLY,
operator=BindingConstraintOperator.EQUAL,
coeffs={"b": [0.3]},
- values="b",
+ values=values_b,
command_context=command_context,
)
assert base.create_diff(other_match) == [
@@ -362,7 +367,7 @@ def test_create_diff(command_context: CommandContext):
time_step=BindingConstraintFrequency.HOURLY,
operator=BindingConstraintOperator.EQUAL,
coeffs={"b": [0.3]},
- values="b",
+ values=values_b,
command_context=command_context,
)
]
diff --git a/tests/variantstudy/model/command/test_manage_district.py b/tests/variantstudy/model/command/test_manage_district.py
index d0ca0b2c4c..fee8be82fe 100644
--- a/tests/variantstudy/model/command/test_manage_district.py
+++ b/tests/variantstudy/model/command/test_manage_district.py
@@ -63,7 +63,6 @@ def test_manage_district(empty_study: FileStudy, command_context: CommandContext
create_district2_command: ICommand = CreateDistrict(
name="One subtracted zone",
- metadata={},
base_filter=DistrictBaseFilter.add_all,
filter_items=[area1_id],
command_context=command_context,
@@ -79,7 +78,6 @@ def test_manage_district(empty_study: FileStudy, command_context: CommandContext
update_district2_command: ICommand = UpdateDistrict(
id="one subtracted zone",
- metadata={},
base_filter=DistrictBaseFilter.remove_all,
filter_items=[area2_id],
command_context=command_context,
@@ -94,7 +92,6 @@ def test_manage_district(empty_study: FileStudy, command_context: CommandContext
create_district3_command: ICommand = CreateDistrict(
name="Empty district without output",
- metadata={},
output=False,
command_context=command_context,
)
diff --git a/tests/variantstudy/model/command/test_replace_matrix.py b/tests/variantstudy/model/command/test_replace_matrix.py
index e3aaad78c4..5436f1e98d 100644
--- a/tests/variantstudy/model/command/test_replace_matrix.py
+++ b/tests/variantstudy/model/command/test_replace_matrix.py
@@ -1,5 +1,7 @@
from unittest.mock import Mock, patch
+import numpy as np
+
from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id
from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy
from antarest.study.storage.variantstudy.business.command_reverter import CommandReverter
@@ -69,18 +71,33 @@ def test_match(command_context: CommandContext):
@patch("antarest.study.storage.variantstudy.business.command_extractor.CommandExtractor.generate_replace_matrix")
def test_revert(mock_generate_replace_matrix, command_context: CommandContext):
- base = ReplaceMatrix(target="foo", matrix=[[0]], command_context=command_context)
+ matrix_a = np.random.rand(5, 2).tolist()
+ base = ReplaceMatrix(target="foo", matrix=matrix_a, command_context=command_context)
study = FileStudy(config=Mock(), tree=Mock())
CommandReverter().revert(base, [], study)
mock_generate_replace_matrix.assert_called_with(study.tree, ["foo"])
assert CommandReverter().revert(
base,
- [ReplaceMatrix(target="foo", matrix="b", command_context=command_context)],
+ [
+ ReplaceMatrix(
+ target="foo",
+ matrix=matrix_a,
+ command_context=command_context,
+ )
+ ],
study,
- ) == [ReplaceMatrix(target="foo", matrix="b", command_context=command_context)]
+ ) == [
+ ReplaceMatrix(
+ target="foo",
+ matrix=matrix_a,
+ command_context=command_context,
+ )
+ ]
def test_create_diff(command_context: CommandContext):
- base = ReplaceMatrix(target="foo", matrix="c", command_context=command_context)
- other_match = ReplaceMatrix(target="foo", matrix="b", command_context=command_context)
+ matrix_a = np.random.rand(5, 2).tolist()
+ base = ReplaceMatrix(target="foo", matrix=matrix_a, command_context=command_context)
+ matrix_b = np.random.rand(5, 2).tolist()
+ other_match = ReplaceMatrix(target="foo", matrix=matrix_b, command_context=command_context)
assert base.create_diff(other_match) == [other_match]
diff --git a/tests/variantstudy/model/test_variant_model.py b/tests/variantstudy/model/test_variant_model.py
index d3a1760077..27cd732e47 100644
--- a/tests/variantstudy/model/test_variant_model.py
+++ b/tests/variantstudy/model/test_variant_model.py
@@ -2,7 +2,9 @@
from pathlib import Path
from unittest.mock import ANY, Mock
+import numpy as np
from sqlalchemy import create_engine
+from sqlalchemy.engine.base import Engine # type: ignore
from antarest.core.cache.business.local_chache import LocalCache
from antarest.core.config import Config, StorageConfig, WorkspaceConfig
@@ -29,17 +31,11 @@
)
-def test_commands_service(tmp_path: Path, command_factory: CommandFactory):
- engine = create_engine(
- "sqlite:///:memory:",
- echo=False,
- connect_args={"check_same_thread": False},
- )
- Base.metadata.create_all(engine)
+def test_commands_service(tmp_path: Path, db_engine: Engine, command_factory: CommandFactory):
# noinspection SpellCheckingInspection
DBSessionMiddleware(
None,
- custom_engine=engine,
+ custom_engine=db_engine,
session_args={"autocommit": False, "autoflush": False},
)
repository = VariantStudyRepository(LocalCache())
@@ -99,12 +95,13 @@ def test_commands_service(tmp_path: Path, command_factory: CommandFactory):
assert len(commands) == 3
# Update command
- # note: we use a matrix reference to simplify tests
+ prepro = np.random.rand(365, 6).tolist()
+ prepro_id = command_factory.command_context.matrix_service.create(prepro)
command_5 = CommandDTO(
action="replace_matrix",
args={
"target": "some/matrix/path",
- "matrix": "matrix://739aa4b6-79ff-4388-8fed-f0d285bfc69f",
+ "matrix": prepro_id,
},
)
service.update_command(
@@ -115,7 +112,7 @@ def test_commands_service(tmp_path: Path, command_factory: CommandFactory):
)
commands = service.get_commands(saved_id, SADMIN)
assert commands[2].action == "replace_matrix"
- assert commands[2].args["matrix"] == "matrix://739aa4b6-79ff-4388-8fed-f0d285bfc69f"
+ assert commands[2].args["matrix"] == prepro_id
# Move command
service.move_command(
diff --git a/webapp/package-lock.json b/webapp/package-lock.json
index e179a3f1ca..6c474d183a 100644
--- a/webapp/package-lock.json
+++ b/webapp/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "antares-web",
- "version": "2.15.3",
+ "version": "2.15.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "antares-web",
- "version": "2.15.1",
+ "version": "2.15.4",
"dependencies": {
"@emotion/react": "11.10.6",
"@emotion/styled": "11.10.6",
diff --git a/webapp/package.json b/webapp/package.json
index 77204beb44..e1157e2109 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -1,6 +1,6 @@
{
"name": "antares-web",
- "version": "2.15.3",
+ "version": "2.15.4",
"private": true,
"engines": {
"node": "18.16.1"
diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json
index e966640063..a6c3b22341 100644
--- a/webapp/public/locales/en/main.json
+++ b/webapp/public/locales/en/main.json
@@ -584,7 +584,6 @@
"studies.variant": "Variant",
"variants.createNewVariant": "Create new variant",
"variants.newVariant": "New variant",
- "variants.error.variantCreation": "Failed to create variant",
"variants.newCommand": "Add new command",
"variants.commandActionLabel": "Select action",
"variants.success.save": "Command updated successfully",
diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json
index 90a338597b..3f8551f4e7 100644
--- a/webapp/public/locales/fr/main.json
+++ b/webapp/public/locales/fr/main.json
@@ -584,7 +584,6 @@
"studies.variant": "Variante",
"variants.createNewVariant": "Créer une nouvelle variante",
"variants.newVariant": "Nouvelle variante",
- "variants.error.variantCreation": "Erreur lors de la création de la variante",
"variants.newCommand": "Ajouter une nouvelle commande",
"variants.commandActionLabel": "Sélectionnez le type",
"variants.success.save": "Commande modifiée avec succès",
diff --git a/webapp/src/components/App/Singlestudy/HomeView/InformationView/CreateVariantDialog.tsx b/webapp/src/components/App/Singlestudy/HomeView/InformationView/CreateVariantDialog.tsx
index 3f6e4f8915..e0578828ae 100644
--- a/webapp/src/components/App/Singlestudy/HomeView/InformationView/CreateVariantDialog.tsx
+++ b/webapp/src/components/App/Singlestudy/HomeView/InformationView/CreateVariantDialog.tsx
@@ -1,12 +1,10 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router";
import { useTranslation } from "react-i18next";
-import { AxiosError } from "axios";
import AddCircleIcon from "@mui/icons-material/AddCircle";
import { GenericInfo, VariantTree } from "../../../../../common/types";
import { createVariant } from "../../../../../services/api/variant";
import { createListFromTree } from "../../../../../services/utils";
-import useEnqueueErrorSnackbar from "../../../../../hooks/useEnqueueErrorSnackbar";
import FormDialog from "../../../../common/dialogs/FormDialog";
import StringFE from "../../../../common/fieldEditors/StringFE";
import Fieldset from "../../../../common/Fieldset";
@@ -24,39 +22,28 @@ function CreateVariantDialog(props: Props) {
const { parentId, open, tree, onClose } = props;
const [t] = useTranslation();
const navigate = useNavigate();
- const enqueueErrorSnackbar = useEnqueueErrorSnackbar();
const [sourceList, setSourceList] = useState>([]);
+ const defaultValues = { name: "", sourceId: parentId };
useEffect(() => {
setSourceList(createListFromTree(tree));
}, [tree]);
- const defaultValues = {
- name: "",
- sourceId: parentId,
- };
-
////////////////////////////////////////////////////////////////
// Event Handlers
////////////////////////////////////////////////////////////////
- const handleSubmit = async (
- data: SubmitHandlerPlus
- ) => {
+ const handleSubmit = (data: SubmitHandlerPlus) => {
const { sourceId, name } = data.values;
+ return createVariant(sourceId, name);
+ };
- try {
- if (sourceId) {
- const variantId = await createVariant(sourceId, name);
- onClose();
- navigate(`/studies/${variantId}`);
- }
- } catch (e) {
- enqueueErrorSnackbar(
- t("variants.error.variantCreation"),
- e as AxiosError
- );
- }
+ const handleSubmitSuccessful = async (
+ data: SubmitHandlerPlus,
+ variantId: string
+ ) => {
+ onClose();
+ navigate(`/studies/${variantId}`);
};
////////////////////////////////////////////////////////////////
@@ -70,6 +57,7 @@ function CreateVariantDialog(props: Props) {
open={open}
onCancel={onClose}
onSubmit={handleSubmit}
+ onSubmitSuccessful={handleSubmitSuccessful}
config={{ defaultValues }}
>
{({ control }) => (
diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/utils.ts b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/utils.ts
index a2bcdd4af9..f140205498 100644
--- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/utils.ts
+++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/utils.ts
@@ -36,7 +36,7 @@ export function createPath(params: Params): string {
const { id, mode } = output;
const isYearPeriod = year && year > 0;
const periodFolder = isYearPeriod
- ? `mc-ind/${Math.max(year, output.nbyears).toString().padStart(5, "0")}`
+ ? `mc-ind/${Math.min(year, output.nbyears).toString().padStart(5, "0")}`
: "mc-all";
const isLink = "area1" in item;
const itemType = isLink ? OutputItemType.Links : OutputItemType.Areas;
diff --git a/webapp/src/components/common/Form/index.tsx b/webapp/src/components/common/Form/index.tsx
index 9e8d6721c2..1c56825c16 100644
--- a/webapp/src/components/common/Form/index.tsx
+++ b/webapp/src/components/common/Form/index.tsx
@@ -51,14 +51,22 @@ export type AutoSubmitConfig = { enable: boolean; wait?: number };
export interface FormProps<
TFieldValues extends FieldValues = FieldValues,
- TContext = any
-> extends Omit, "onSubmit" | "children"> {
+ TContext = any,
+ SubmitReturnValue = any
+> extends Omit<
+ React.HTMLAttributes,
+ "onSubmit" | "onInvalid" | "children"
+ > {
config?: UseFormProps;
onSubmit?: (
data: SubmitHandlerPlus,
event?: React.BaseSyntheticEvent
- ) => void | Promise;
- onSubmitError?: SubmitErrorHandler;
+ ) => void | Promise;
+ onSubmitSuccessful?: (
+ data: SubmitHandlerPlus,
+ submitResult: SubmitReturnValue
+ ) => void;
+ onInvalid?: SubmitErrorHandler;
children:
| ((formApi: UseFormReturnPlus) => React.ReactNode)
| React.ReactNode;
@@ -82,7 +90,8 @@ function Form(
const {
config,
onSubmit,
- onSubmitError,
+ onSubmitSuccessful,
+ onInvalid,
children,
submitButtonText,
submitButtonIcon,
@@ -110,6 +119,8 @@ function Form(
[]
);
const lastSubmittedData = useRef();
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ const submitSuccessfulCb = useRef(() => {});
const preventClose = useRef(false);
const contextValue = useMemo(
() => ({ isAutoSubmitEnabled: autoSubmitConfig.enable }),
@@ -177,6 +188,8 @@ function Form(
useEffect(
() => {
if (isSubmitSuccessful && lastSubmittedData.current) {
+ submitSuccessfulCb.current();
+
const valuesToSetAfterReset = getValues(
fieldsChangeDuringAutoSubmitting.current
);
@@ -232,11 +245,11 @@ function Form(
typeof data
>;
- const res = [];
+ const toResolve = [];
if (autoSubmitConfig.enable) {
const listeners = fieldAutoSubmitListeners.current;
- res.push(
+ toResolve.push(
...Object.keys(listeners)
.filter((key) => R.hasPath(stringToPath(key), dirtyValues))
.map((key) => {
@@ -246,11 +259,19 @@ function Form(
);
}
+ const dataArg = { values: data, dirtyValues };
+
if (onSubmit) {
- res.push(onSubmit({ values: data, dirtyValues }, event));
+ toResolve.push(onSubmit(dataArg, event));
}
- return Promise.all(res)
+ return Promise.all(toResolve)
+ .then((values) => {
+ submitSuccessfulCb.current = () => {
+ const onSubmitRes = onSubmit ? R.last(values) : undefined;
+ onSubmitSuccessful?.(dataArg, onSubmitRes);
+ };
+ })
.catch((err) => {
enqueueErrorSnackbar(t("form.submit.error"), err);
@@ -266,7 +287,7 @@ function Form(
.finally(() => {
preventClose.current = false;
});
- }, onSubmitError);
+ }, onInvalid);
return callback();
};
diff --git a/webapp/src/components/common/FormTable/index.tsx b/webapp/src/components/common/FormTable/index.tsx
index 41bd13812f..b89380dd40 100644
--- a/webapp/src/components/common/FormTable/index.tsx
+++ b/webapp/src/components/common/FormTable/index.tsx
@@ -23,7 +23,7 @@ export interface FormTableProps<
> {
defaultValues: DefaultValues;
onSubmit?: FormProps["onSubmit"];
- onSubmitError?: FormProps["onSubmitError"];
+ onInvalid?: FormProps["onInvalid"];
formApiRef?: FormProps["apiRef"];
sx?: SxProps;
tableProps?: Omit & {
@@ -40,7 +40,7 @@ function FormTable(
const {
defaultValues,
onSubmit,
- onSubmitError,
+ onInvalid,
sx,
formApiRef,
tableProps = {},
@@ -83,7 +83,7 @@ function FormTable(