diff --git a/antarest/__init__.py b/antarest/__init__.py index 9e6608e87c..c3d6be21fb 100644 --- a/antarest/__init__.py +++ b/antarest/__init__.py @@ -7,9 +7,9 @@ # Standard project metadata -__version__ = "2.16.4" +__version__ = "2.16.5" __author__ = "RTE, Antares Web Team" -__date__ = "2024-02-14" +__date__ = "2024-02-29" # noinspection SpellCheckingInspection __credits__ = "(c) Réseau de Transport de l’Électricité (RTE)" diff --git a/antarest/study/model.py b/antarest/study/model.py index fe10b4f211..fe993a1512 100644 --- a/antarest/study/model.py +++ b/antarest/study/model.py @@ -188,8 +188,9 @@ class Study(Base): # type: ignore __mapper_args__ = {"polymorphic_identity": "study", "polymorphic_on": type} def __str__(self) -> str: + cls = self.__class__.__name__ return ( - f"[Study]" + f"[{cls}]" f" id={self.id}," f" type={self.type}," f" name={self.name}," diff --git a/antarest/study/storage/patch_service.py b/antarest/study/storage/patch_service.py index e3ece9071e..c52752ae25 100644 --- a/antarest/study/storage/patch_service.py +++ b/antarest/study/storage/patch_service.py @@ -1,27 +1,32 @@ -import logging +import json +import typing as t from pathlib import Path -from typing import Optional, Union from antarest.study.model import Patch, PatchOutputs, RawStudy, StudyAdditionalData from antarest.study.repository import StudyMetadataRepository from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.model.dbmodel import VariantStudy -logger = logging.getLogger(__name__) +PATCH_JSON = "patch.json" class PatchService: - def __init__(self, repository: Optional[StudyMetadataRepository] = None): + """ + Handle patch file ("patch.json") for a RawStudy or VariantStudy + """ + + def __init__(self, repository: t.Optional[StudyMetadataRepository] = None): self.repository = repository - def get(self, study: Union[RawStudy, VariantStudy], get_from_file: bool = False) -> Patch: - if not get_from_file: + def get(self, study: t.Union[RawStudy, VariantStudy], get_from_file: bool = False) -> Patch: + if not get_from_file and study.additional_data is not None: # the `study.additional_data.patch` field is optional - if patch_data := study.additional_data.patch: - return Patch.parse_raw(patch_data) + if study.additional_data.patch: + patch_obj = json.loads(study.additional_data.patch or "{}") + return Patch.parse_obj(patch_obj) patch = Patch() - patch_path = Path(study.path) / "patch.json" + patch_path = Path(study.path) / PATCH_JSON if patch_path.exists(): patch = Patch.parse_file(patch_path) @@ -29,14 +34,14 @@ def get(self, study: Union[RawStudy, VariantStudy], get_from_file: bool = False) def get_from_filestudy(self, file_study: FileStudy) -> Patch: patch = Patch() - patch_path = (Path(file_study.config.study_path)) / "patch.json" + patch_path = (Path(file_study.config.study_path)) / PATCH_JSON if patch_path.exists(): patch = Patch.parse_file(patch_path) return patch def set_reference_output( self, - study: Union[RawStudy, VariantStudy], + study: t.Union[RawStudy, VariantStudy], output_id: str, status: bool = True, ) -> None: @@ -47,12 +52,12 @@ def set_reference_output( patch.outputs = PatchOutputs(reference=output_id) self.save(study, patch) - def save(self, study: Union[RawStudy, VariantStudy], patch: Patch) -> None: + def save(self, study: t.Union[RawStudy, VariantStudy], patch: Patch) -> None: if self.repository: study.additional_data = study.additional_data or StudyAdditionalData() study.additional_data.patch = patch.json() self.repository.save(study) - patch_path = (Path(study.path)) / "patch.json" + patch_path = (Path(study.path)) / PATCH_JSON patch_path.parent.mkdir(parents=True, exist_ok=True) patch_path.write_text(patch.json()) diff --git a/antarest/study/storage/rawstudy/raw_study_service.py b/antarest/study/storage/rawstudy/raw_study_service.py index f5f98c97e5..4990d70bf6 100644 --- a/antarest/study/storage/rawstudy/raw_study_service.py +++ b/antarest/study/storage/rawstudy/raw_study_service.py @@ -1,10 +1,10 @@ import logging import shutil import time +import typing as t from datetime import datetime from pathlib import Path from threading import Thread -from typing import BinaryIO, List, Optional, Sequence from uuid import uuid4 from zipfile import ZipFile @@ -61,7 +61,7 @@ def __init__( ) self.cleanup_thread.start() - def update_from_raw_meta(self, metadata: RawStudy, fallback_on_default: Optional[bool] = False) -> None: + def update_from_raw_meta(self, metadata: RawStudy, fallback_on_default: t.Optional[bool] = False) -> None: """ Update metadata from study raw metadata Args: @@ -90,7 +90,7 @@ def update_from_raw_meta(self, metadata: RawStudy, fallback_on_default: Optional metadata.version = metadata.version or 0 metadata.created_at = metadata.created_at or datetime.utcnow() metadata.updated_at = metadata.updated_at or datetime.utcnow() - if not metadata.additional_data: + if metadata.additional_data is None: metadata.additional_data = StudyAdditionalData() metadata.additional_data.patch = metadata.additional_data.patch or Patch().json() metadata.additional_data.author = metadata.additional_data.author or "Unknown" @@ -148,7 +148,7 @@ def get_raw( self, metadata: RawStudy, use_cache: bool = True, - output_dir: Optional[Path] = None, + output_dir: t.Optional[Path] = None, ) -> FileStudy: """ Fetch a study object and its config @@ -163,7 +163,7 @@ def get_raw( study_path = self.get_study_path(metadata) return self.study_factory.create_from_fs(study_path, metadata.id, output_dir, use_cache=use_cache) - def get_synthesis(self, metadata: RawStudy, params: Optional[RequestParameters] = None) -> FileStudyTreeConfigDTO: + def get_synthesis(self, metadata: RawStudy, params: t.Optional[RequestParameters] = None) -> FileStudyTreeConfigDTO: self._check_study_exists(metadata) study_path = self.get_study_path(metadata) study = self.study_factory.create_from_fs(study_path, metadata.id) @@ -206,7 +206,7 @@ def copy( self, src_meta: RawStudy, dest_name: str, - groups: Sequence[str], + groups: t.Sequence[str], with_outputs: bool = False, ) -> RawStudy: """ @@ -223,7 +223,7 @@ def copy( """ self._check_study_exists(src_meta) - if not src_meta.additional_data: + if src_meta.additional_data is None: additional_data = StudyAdditionalData() else: additional_data = StudyAdditionalData( @@ -295,7 +295,7 @@ def delete_output(self, metadata: RawStudy, output_name: str) -> None: output_path.unlink(missing_ok=True) remove_from_cache(self.cache, metadata.id) - def import_study(self, metadata: RawStudy, stream: BinaryIO) -> Study: + def import_study(self, metadata: RawStudy, stream: t.BinaryIO) -> Study: """ Import study in the directory of the study. @@ -329,7 +329,7 @@ def export_study_flat( metadata: RawStudy, dst_path: Path, outputs: bool = True, - output_list_filter: Optional[List[str]] = None, + output_list_filter: t.Optional[t.List[str]] = None, denormalize: bool = True, ) -> None: try: @@ -352,7 +352,7 @@ def export_study_flat( def check_errors( self, metadata: RawStudy, - ) -> List[str]: + ) -> t.List[str]: """ Check study antares data integrity Args: diff --git a/antarest/study/storage/utils.py b/antarest/study/storage/utils.py index a0fc7a02fe..8e9a9a573d 100644 --- a/antarest/study/storage/utils.py +++ b/antarest/study/storage/utils.py @@ -52,7 +52,7 @@ def update_antares_info(metadata: Study, study_tree: FileStudyTree, *, update_au study_data_info["antares"]["created"] = metadata.created_at.timestamp() study_data_info["antares"]["lastsave"] = metadata.updated_at.timestamp() study_data_info["antares"]["version"] = metadata.version - if update_author: + if update_author and metadata.additional_data: study_data_info["antares"]["author"] = metadata.additional_data.author study_tree.save(study_data_info, ["study"]) diff --git a/antarest/study/storage/variantstudy/model/dbmodel.py b/antarest/study/storage/variantstudy/model/dbmodel.py index bbe264f89f..15b1bb896d 100644 --- a/antarest/study/storage/variantstudy/model/dbmodel.py +++ b/antarest/study/storage/variantstudy/model/dbmodel.py @@ -112,3 +112,7 @@ def is_snapshot_up_to_date(self) -> bool: and (self.snapshot.created_at >= self.updated_at) and (self.snapshot_dir / "study.antares").is_file() ) + + def has_snapshot(self) -> bool: + """Check if the snapshot exists.""" + return (self.snapshot is not None) and (self.snapshot_dir / "study.antares").is_file() diff --git a/antarest/study/storage/variantstudy/snapshot_generator.py b/antarest/study/storage/variantstudy/snapshot_generator.py index 50972ae99a..138089a35e 100644 --- a/antarest/study/storage/variantstudy/snapshot_generator.py +++ b/antarest/study/storage/variantstudy/snapshot_generator.py @@ -246,7 +246,7 @@ def search_ref_study( # To reuse the snapshot of the current variant, the last executed command # must be one of the commands of the current variant. curr_variant = descendants[-1] - if curr_variant.snapshot: + if curr_variant.has_snapshot(): last_exec_cmd = curr_variant.snapshot.last_executed_command command_ids = [c.id for c in curr_variant.commands] # If the variant has no command, we can reuse the snapshot if it is recent diff --git a/antarest/study/storage/variantstudy/variant_study_service.py b/antarest/study/storage/variantstudy/variant_study_service.py index e59ef3fa94..582e251013 100644 --- a/antarest/study/storage/variantstudy/variant_study_service.py +++ b/antarest/study/storage/variantstudy/variant_study_service.py @@ -3,10 +3,10 @@ import logging import re import shutil +import typing as t from datetime import datetime from functools import reduce from pathlib import Path -from typing import Callable, List, Optional, Sequence, Tuple, cast from uuid import uuid4 from fastapi import HTTPException @@ -101,11 +101,11 @@ def get_command(self, study_id: str, command_id: str, params: RequestParameters) try: index = [command.id for command in study.commands].index(command_id) # Maybe add Try catch for this - return cast(CommandDTO, study.commands[index].to_dto()) + return t.cast(CommandDTO, study.commands[index].to_dto()) except ValueError: raise CommandNotFoundError(f"Command with id {command_id} not found") from None - def get_commands(self, study_id: str, params: RequestParameters) -> List[CommandDTO]: + def get_commands(self, study_id: str, params: RequestParameters) -> t.List[CommandDTO]: """ Get command lists Args: @@ -116,8 +116,8 @@ def get_commands(self, study_id: str, params: RequestParameters) -> List[Command study = self._get_variant_study(study_id, params) return [command.to_dto() for command in study.commands] - def _check_commands_validity(self, study_id: str, commands: List[CommandDTO]) -> List[ICommand]: - command_objects: List[ICommand] = [] + def _check_commands_validity(self, study_id: str, commands: t.List[CommandDTO]) -> t.List[ICommand]: + command_objects: t.List[ICommand] = [] for i, command in enumerate(commands): try: command_objects.extend(self.command_factory.to_command(command)) @@ -157,9 +157,9 @@ def append_command(self, study_id: str, command: CommandDTO, params: RequestPara def append_commands( self, study_id: str, - commands: List[CommandDTO], + commands: t.List[CommandDTO], params: RequestParameters, - ) -> List[str]: + ) -> t.List[str]: """ Add command to list of commands (at the end) Args: @@ -196,7 +196,7 @@ def append_commands( def replace_commands( self, study_id: str, - commands: List[CommandDTO], + commands: t.List[CommandDTO], params: RequestParameters, ) -> str: """ @@ -320,13 +320,13 @@ def export_commands_matrices(self, study_id: str, params: RequestParameters) -> lambda: reduce( lambda m, c: m + c.get_inner_matrices(), self.command_factory.to_command(command.to_dto()), - cast(List[str], []), + t.cast(t.List[str], []), ), lambda e: logger.warning(f"Failed to parse command {command}", exc_info=e), ) or [] } - return cast(MatrixService, self.command_factory.command_context.matrix_service).download_matrix_list( + return t.cast(MatrixService, self.command_factory.command_context.matrix_service).download_matrix_list( list(matrices), f"{study.name}_{study.id}_matrices", params ) @@ -410,7 +410,7 @@ def get_all_variants_children( def walk_children( self, parent_id: str, - fun: Callable[[VariantStudy], None], + fun: t.Callable[[VariantStudy], None], bottom_first: bool, ) -> None: study = self._get_variant_study( @@ -426,13 +426,13 @@ def walk_children( if bottom_first: fun(study) - def get_variants_parents(self, id: str, params: RequestParameters) -> List[StudyMetadataDTO]: - output_list: List[StudyMetadataDTO] = self._get_variants_parents(id, params) + def get_variants_parents(self, id: str, params: RequestParameters) -> t.List[StudyMetadataDTO]: + output_list: t.List[StudyMetadataDTO] = self._get_variants_parents(id, params) if output_list: output_list = output_list[1:] return output_list - def get_direct_parent(self, id: str, params: RequestParameters) -> Optional[StudyMetadataDTO]: + def get_direct_parent(self, id: str, params: RequestParameters) -> t.Optional[StudyMetadataDTO]: study = self._get_variant_study(id, params, raw_study_accepted=True) if study.parent_id is not None: parent = self._get_variant_study(study.parent_id, params, raw_study_accepted=True) @@ -447,7 +447,7 @@ def get_direct_parent(self, id: str, params: RequestParameters) -> Optional[Stud ) return None - def _get_variants_parents(self, id: str, params: RequestParameters) -> List[StudyMetadataDTO]: + def _get_variants_parents(self, id: str, params: RequestParameters) -> t.List[StudyMetadataDTO]: study = self._get_variant_study(id, params, raw_study_accepted=True) metadata = ( self.get_study_information( @@ -458,7 +458,7 @@ def _get_variants_parents(self, id: str, params: RequestParameters) -> List[Stud study, ) ) - output_list: List[StudyMetadataDTO] = [metadata] + output_list: t.List[StudyMetadataDTO] = [metadata] if study.parent_id is not None: output_list.extend( self._get_variants_parents( @@ -530,16 +530,15 @@ def create_variant_study(self, uuid: str, name: str, params: RequestParameters) assert_permission(params.user, study, StudyPermissionType.READ) new_id = str(uuid4()) study_path = str(self.config.get_workspace_path() / new_id) - if study.additional_data: - # noinspection PyArgumentList + if study.additional_data is None: + additional_data = StudyAdditionalData() + else: additional_data = StudyAdditionalData( horizon=study.additional_data.horizon, author=study.additional_data.author, patch=study.additional_data.patch, ) - else: - additional_data = StudyAdditionalData() - # noinspection PyArgumentList + variant_study = VariantStudy( id=new_id, name=name, @@ -653,7 +652,7 @@ def generate_study_config( self, variant_study_id: str, params: RequestParameters, - ) -> Tuple[GenerationResultInfoDTO, FileStudyTreeConfig]: + ) -> t.Tuple[GenerationResultInfoDTO, FileStudyTreeConfig]: # Get variant study variant_study = self._get_variant_study(variant_study_id, params) @@ -667,8 +666,8 @@ def _generate_study_config( self, original_study: VariantStudy, metadata: VariantStudy, - config: Optional[FileStudyTreeConfig], - ) -> Tuple[GenerationResultInfoDTO, FileStudyTreeConfig]: + config: t.Optional[FileStudyTreeConfig], + ) -> t.Tuple[GenerationResultInfoDTO, FileStudyTreeConfig]: parent_study = self.repository.get(metadata.parent_id) if parent_study is None: raise StudyNotFoundError(metadata.parent_id) @@ -698,9 +697,9 @@ def _get_commands_and_notifier( variant_study: VariantStudy, notifier: TaskUpdateNotifier, from_index: int = 0, - ) -> Tuple[List[List[ICommand]], Callable[[int, bool, str], None]]: + ) -> t.Tuple[t.List[t.List[ICommand]], t.Callable[[int, bool, str], None]]: # Generate - commands: List[List[ICommand]] = self._to_commands(variant_study, from_index) + commands: t.List[t.List[ICommand]] = self._to_commands(variant_study, from_index) def notify(command_index: int, command_result: bool, command_message: str) -> None: try: @@ -727,8 +726,8 @@ def notify(command_index: int, command_result: bool, command_message: str) -> No return commands, notify - def _to_commands(self, metadata: VariantStudy, from_index: int = 0) -> List[List[ICommand]]: - commands: List[List[ICommand]] = [ + def _to_commands(self, metadata: VariantStudy, from_index: int = 0) -> t.List[t.List[ICommand]]: + commands: t.List[t.List[ICommand]] = [ self.command_factory.to_command(command_block.to_dto()) for index, command_block in enumerate(metadata.commands) if from_index <= index @@ -740,7 +739,7 @@ def _generate_config( variant_study: VariantStudy, config: FileStudyTreeConfig, notifier: TaskUpdateNotifier = noop_notifier, - ) -> Tuple[GenerationResultInfoDTO, FileStudyTreeConfig]: + ) -> t.Tuple[GenerationResultInfoDTO, FileStudyTreeConfig]: commands, notify = self._get_commands_and_notifier(variant_study=variant_study, notifier=notifier) return self.generator.generate_config(commands, config, variant_study, notifier=notify) @@ -809,7 +808,7 @@ def copy( self, src_meta: VariantStudy, dest_name: str, - groups: Sequence[str], + groups: t.Sequence[str], with_outputs: bool = False, ) -> VariantStudy: """ @@ -826,16 +825,14 @@ def copy( """ new_id = str(uuid4()) study_path = str(self.config.get_workspace_path() / new_id) - if src_meta.additional_data: - # noinspection PyArgumentList + if src_meta.additional_data is None: + additional_data = StudyAdditionalData() + else: additional_data = StudyAdditionalData( horizon=src_meta.additional_data.horizon, author=src_meta.additional_data.author, patch=src_meta.additional_data.patch, ) - else: - additional_data = StudyAdditionalData() - # noinspection PyArgumentList dst_meta = VariantStudy( id=new_id, name=dest_name, @@ -893,7 +890,7 @@ def _safe_generation(self, metadata: VariantStudy, timeout: int = DEFAULT_AWAIT_ @staticmethod def _get_snapshot_last_executed_command_index( study: VariantStudy, - ) -> Optional[int]: + ) -> t.Optional[int]: if study.snapshot and study.snapshot.last_executed_command: last_executed_command_index = [command.id for command in study.commands].index( study.snapshot.last_executed_command @@ -905,7 +902,7 @@ def get_raw( self, metadata: VariantStudy, use_cache: bool = True, - output_dir: Optional[Path] = None, + output_dir: t.Optional[Path] = None, ) -> FileStudy: """ Fetch a study raw tree object and its config @@ -925,7 +922,7 @@ def get_raw( use_cache=use_cache, ) - def get_study_sim_result(self, study: VariantStudy) -> List[StudySimResultDTO]: + def get_study_sim_result(self, study: VariantStudy) -> t.List[StudySimResultDTO]: """ Get global result information Args: @@ -988,7 +985,7 @@ def export_study_flat( metadata: VariantStudy, dst_path: Path, outputs: bool = True, - output_list_filter: Optional[List[str]] = None, + output_list_filter: t.Optional[t.List[str]] = None, denormalize: bool = True, ) -> None: self._safe_generation(metadata) @@ -1009,7 +1006,7 @@ def export_study_flat( def get_synthesis( self, metadata: VariantStudy, - params: Optional[RequestParameters] = None, + params: t.Optional[RequestParameters] = None, ) -> FileStudyTreeConfigDTO: """ Return study synthesis diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1399b80670..b4dbdda4e0 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,29 @@ Antares Web Changelog ===================== +v2.16.5 (2024-02-29) +-------------------- + +### Features + +* **ui-results:** add results matrix timestamps [`#1945`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1945) + + +### Bug Fixes + +* **ui-hydro:** add missing matrix path encoding [`#1940`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1940) +* **ui-thermal:** update cluster group options to handle `Other 1` [`#1945`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1945) +* **ui-results:** prevent duplicate updates on same toggle button click [`#1945`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1945) +* **ui-results:** disable stretch to fix display issue [`#1945`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1945) +* **ui-hydro:** disable stretch to fix display issue on some matrices [`#1945`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1945) +* **variants:** correct the generation of variant when a snapshot is removed [`#1947`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1947) +* **tags:** resolve issue with `study.additional_data.patch` attribute reading [`#1944`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1944) +* **study:** correct access to study `additional_data` (#1949) [`#1949`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1949) +* **ui-tablemode:** create modal is frozen when submitting without column [`#1946`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1946) +* **ui-tablemode:** 'co2' column not working [`#1952`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1952) +* **ui:** add missing i18n dependency [`#1954`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1954) + + v2.16.4 (2024-02-14) -------------------- diff --git a/setup.py b/setup.py index dc092ab8c7..1e0d76dfe5 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="AntaREST", - version="2.16.4", + version="2.16.5", 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 8f0a02dd21..beb967df7e 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.16.4 +sonar.projectVersion=2.16.5 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/storage/business/test_patch_service.py b/tests/storage/business/test_patch_service.py index 7f792a057b..ed7dd6c444 100644 --- a/tests/storage/business/test_patch_service.py +++ b/tests/storage/business/test_patch_service.py @@ -51,7 +51,7 @@ class TestPatchService: @with_db_context @pytest.mark.parametrize("get_from_file", [True, False]) @pytest.mark.parametrize("file_data", ["", PATCH_CONTENT]) - @pytest.mark.parametrize("patch_data", ["", PATCH_CONTENT]) + @pytest.mark.parametrize("patch_data", [None, "", PATCH_CONTENT]) def test_get( self, tmp_path: Path, @@ -67,7 +67,15 @@ def test_get( patch_json.write_text(file_data, encoding="utf-8") # Prepare a RAW study - # noinspection PyArgumentList + additional_data = ( + None + if patch_data is None + else StudyAdditionalData( + author="john.doe", + horizon="foo-horizon", + patch=patch_data, + ) + ) raw_study = RawStudy( id=study_id, name="my_study", @@ -76,11 +84,7 @@ def test_get( created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), version="840", - additional_data=StudyAdditionalData( - author="john.doe", - horizon="foo-horizon", - patch=patch_data, - ), + additional_data=additional_data, archived=False, owner=None, groups=[], diff --git a/tests/study/storage/variantstudy/model/test_dbmodel.py b/tests/study/storage/variantstudy/model/test_dbmodel.py index 0715dec535..1e18e4fef1 100644 --- a/tests/study/storage/variantstudy/model/test_dbmodel.py +++ b/tests/study/storage/variantstudy/model/test_dbmodel.py @@ -188,7 +188,7 @@ def test_init__without_snapshot(self, db_session: Session, raw_study_id: str, us obj: VariantStudy = db_session.query(VariantStudy).filter(VariantStudy.id == variant_study_id).one() # check Study representation - assert str(obj).startswith(f"[Study] id={variant_study_id}") + assert str(obj).startswith(f"[VariantStudy] id={variant_study_id}") # check Study fields assert obj.id == variant_study_id diff --git a/webapp/package-lock.json b/webapp/package-lock.json index cfc4573790..8a6261057b 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -1,12 +1,12 @@ { "name": "antares-web", - "version": "2.16.4", + "version": "2.16.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "antares-web", - "version": "2.16.4", + "version": "2.16.5", "dependencies": { "@emotion/react": "11.11.1", "@emotion/styled": "11.11.0", diff --git a/webapp/package.json b/webapp/package.json index 4699b94a7c..be76665a99 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -1,6 +1,6 @@ { "name": "antares-web", - "version": "2.16.4", + "version": "2.16.5", "private": true, "type": "module", "scripts": { diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx index 2a8796074c..12d8a773d3 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx @@ -32,6 +32,7 @@ function HydroMatrix({ type }: Props) { fetchFn={hydroMatrix.fetchFn} disableEdit={hydroMatrix.disableEdit} enablePercentDisplay={hydroMatrix.enablePercentDisplay} + stretch={hydroMatrix.stretch} /> ); diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts index 8fc143d280..008d271eb2 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts @@ -36,6 +36,7 @@ export interface HydroMatrixProps { fetchFn?: fetchMatrixFn; disableEdit?: boolean; enablePercentDisplay?: boolean; + stretch?: boolean; // TODO: Remove this once the `EditableMatrix` component is refactored } type Matrices = Record; @@ -120,6 +121,7 @@ export const MATRICES: Matrices = { "Pumping Max Energy (Hours at Pmax)", ], stats: MatrixStats.NOCOL, + stretch: false, }, [HydroMatrixType.ReservoirLevels]: { title: "Reservoir Levels", @@ -175,6 +177,7 @@ export const MATRICES: Matrices = { "December", ], stats: MatrixStats.NOCOL, + stretch: false, }, [HydroMatrixType.Allocation]: { title: "Allocation", diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/utils.ts index 68284b81f3..d113e06c4f 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/utils.ts @@ -16,7 +16,7 @@ export const THERMAL_GROUPS = [ "Mixed fuel", "Nuclear", "Oil", - "Other", + "Other 1", "Other 2", "Other 3", "Other 4", diff --git a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx index a22ba12022..e61c2ac3d2 100644 --- a/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Results/ResultDetails/index.tsx @@ -49,6 +49,7 @@ import ButtonBack from "../../../../../common/ButtonBack"; import BooleanFE from "../../../../../common/fieldEditors/BooleanFE"; import SelectFE from "../../../../../common/fieldEditors/SelectFE"; import NumberFE from "../../../../../common/fieldEditors/NumberFE"; +import moment from "moment"; function ResultDetails() { const { study } = useOutletContext<{ study: StudyMetadata }>(); @@ -148,15 +149,27 @@ function ResultDetails() { }, ); + // !NOTE: Workaround to display the date in the correct format, to be replaced by a proper solution. + const dateTimeFromIndex = useMemo(() => { + if (!matrixRes.data) return []; + + return matrixRes.data.index.map((dateTime) => { + const parsedDate = moment(dateTime, "MM/DD HH:mm"); + return parsedDate.format("ddd D MMM HH:mm"); + }); + }, [matrixRes.data]); + //////////////////////////////////////////////////////////////// // Event Handlers //////////////////////////////////////////////////////////////// const handleItemTypeChange: ToggleButtonGroupProps["onChange"] = ( _, - value: OutputItemType, + newValue: OutputItemType, ) => { - setItemType(value); + if (newValue && newValue !== itemType) { + setItemType(newValue); + } }; const handleDownload = (matrixData: MatrixType, fileName: string): void => { @@ -366,6 +379,8 @@ function ResultDetails() { ) diff --git a/webapp/src/components/App/Singlestudy/index.tsx b/webapp/src/components/App/Singlestudy/index.tsx index 9a76747915..1900d81cfb 100644 --- a/webapp/src/components/App/Singlestudy/index.tsx +++ b/webapp/src/components/App/Singlestudy/index.tsx @@ -71,7 +71,7 @@ function SingleStudy(props: Props) { path: `/studies/${studyId}/explore/debug`, }, ], - [studyId], + [studyId, t], ); const updateStudyData = useCallback(async () => { diff --git a/webapp/src/components/common/EditableMatrix/index.tsx b/webapp/src/components/common/EditableMatrix/index.tsx index 0d1c9e21b4..87aa8441f2 100644 --- a/webapp/src/components/common/EditableMatrix/index.tsx +++ b/webapp/src/components/common/EditableMatrix/index.tsx @@ -28,6 +28,7 @@ interface PropTypes { rowNames?: string[]; computStats?: MatrixStats; isPercentDisplayEnabled?: boolean; + stretch?: boolean; } type CellType = Array; @@ -55,6 +56,7 @@ function EditableMatrix(props: PropTypes) { rowNames, computStats, isPercentDisplayEnabled = false, + stretch = true, } = props; const { data = [], columns = [], index = [] } = matrix; const prependIndex = index.length > 0 && matrixTime; @@ -176,7 +178,7 @@ function EditableMatrix(props: PropTypes) { data={grid} width="100%" height="100%" - stretchH="all" + stretchH={stretch ? "all" : "none"} className="editableMatrix" colHeaders rowHeaderWidth={matrixRowNames ? 150 : undefined} @@ -186,7 +188,6 @@ function EditableMatrix(props: PropTypes) { beforeKeyDown={(e) => handleKeyDown(e)} columns={formattedColumns} rowHeaders={matrixRowNames || true} - manualColumnResize /> ); diff --git a/webapp/src/components/common/EditableMatrix/style.ts b/webapp/src/components/common/EditableMatrix/style.ts index da7da1910b..dd62aecfd2 100644 --- a/webapp/src/components/common/EditableMatrix/style.ts +++ b/webapp/src/components/common/EditableMatrix/style.ts @@ -6,7 +6,7 @@ export const Root = styled(Box)(({ theme }) => ({ display: "flex", flexDirection: "column", alignItems: "center", - overflow: "auto", + overflow: "hidden", })); export const StyledButton = styled(Button)(({ theme }) => ({ diff --git a/webapp/src/components/common/MatrixInput/index.tsx b/webapp/src/components/common/MatrixInput/index.tsx index 0d43522296..7807cd94c4 100644 --- a/webapp/src/components/common/MatrixInput/index.tsx +++ b/webapp/src/components/common/MatrixInput/index.tsx @@ -38,6 +38,7 @@ interface Props { fetchFn?: fetchMatrixFn; disableEdit?: boolean; enablePercentDisplay?: boolean; + stretch?: boolean; } function MatrixInput({ @@ -50,6 +51,7 @@ function MatrixInput({ fetchFn, disableEdit, enablePercentDisplay, + stretch, }: Props) { const { enqueueSnackbar } = useSnackbar(); const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); @@ -211,6 +213,7 @@ function MatrixInput({ onUpdate={handleUpdate} computStats={computStats} isPercentDisplayEnabled={enablePercentDisplay} + stretch={stretch} /> ) : ( !isLoading && ( diff --git a/webapp/src/components/common/fieldEditors/ListFE/index.tsx b/webapp/src/components/common/fieldEditors/ListFE/index.tsx index 5d3048b2ec..d72fa71c81 100644 --- a/webapp/src/components/common/fieldEditors/ListFE/index.tsx +++ b/webapp/src/components/common/fieldEditors/ListFE/index.tsx @@ -34,9 +34,10 @@ import reactHookFormSupport, { import { createFakeBlurEventHandler, createFakeChangeEventHandler, + createFakeInputElement, FakeBlurEventHandler, FakeChangeEventHandler, - FakeHTMLInputElement, + InputObject, } from "../../../../utils/feUtils"; import { makeLabel, makeListItems } from "./utils"; @@ -97,7 +98,7 @@ function ListFE(props: ListFEProps) { // Trigger event handlers useUpdateEffect(() => { if (onChange || onBlur) { - const fakeInputElement: FakeHTMLInputElement = { + const fakeInputElement: InputObject = { value: listItems.map((item) => item.value), name, }; @@ -108,10 +109,10 @@ function ListFE(props: ListFEProps) { // Set ref useEffect(() => { - const fakeInputElement: FakeHTMLInputElement = { + const fakeInputElement = createFakeInputElement({ value: listItems.map((item) => item.value), name, - }; + }); setRef(inputRef, fakeInputElement); }, [inputRef, listItems, name]); diff --git a/webapp/src/services/api/matrix.ts b/webapp/src/services/api/matrix.ts index 9b2d4f5213..1eff05f7c9 100644 --- a/webapp/src/services/api/matrix.ts +++ b/webapp/src/services/api/matrix.ts @@ -97,7 +97,7 @@ export const editMatrix = async ( matrixEdit: MatrixEditDTO[], ): Promise => { const res = await client.put( - `/v1/studies/${sid}/matrix?path=${path}`, + `/v1/studies/${sid}/matrix?path=${encodeURIComponent(path)}`, matrixEdit, ); return res.data; diff --git a/webapp/src/services/api/studies/tableMode/index.ts b/webapp/src/services/api/studies/tableMode/index.ts index cd77a71891..c6c4db5b80 100644 --- a/webapp/src/services/api/studies/tableMode/index.ts +++ b/webapp/src/services/api/studies/tableMode/index.ts @@ -1,10 +1,10 @@ -import { snakeCase } from "lodash"; import { DeepPartial } from "react-hook-form"; import { StudyMetadata } from "../../../../common/types"; import client from "../../client"; import { format } from "../../../../utils/stringUtils"; import { TABLE_MODE_API_URL } from "../../constants"; import type { TableData, TableModeColumnsForType, TableModeType } from "./type"; +import { toColumnApiName } from "./utils"; export async function getTableMode( studyId: StudyMetadata["id"], @@ -15,7 +15,7 @@ export async function getTableMode( const res = await client.get(url, { params: { table_type: type, - columns: columns.map(snakeCase).join(","), + columns: columns.map(toColumnApiName).join(","), }, }); return res.data; diff --git a/webapp/src/services/api/studies/tableMode/utils.ts b/webapp/src/services/api/studies/tableMode/utils.ts new file mode 100644 index 0000000000..e856785502 --- /dev/null +++ b/webapp/src/services/api/studies/tableMode/utils.ts @@ -0,0 +1,11 @@ +import { snakeCase } from "lodash"; +import { TableModeColumnsForType, TableModeType } from "./type"; + +export function toColumnApiName( + column: TableModeColumnsForType[number], +) { + if (column === "co2") { + return "co2"; + } + return snakeCase(column); +} diff --git a/webapp/src/utils/feUtils.ts b/webapp/src/utils/feUtils.ts index 8c3bd9f25b..636fcb82ea 100644 --- a/webapp/src/utils/feUtils.ts +++ b/webapp/src/utils/feUtils.ts @@ -1,13 +1,13 @@ import { HTMLInputTypeAttribute } from "react"; -export interface FakeHTMLInputElement { +export interface InputObject { value: unknown; checked?: boolean; type?: HTMLInputTypeAttribute; name?: string; } -type Target = HTMLInputElement | FakeHTMLInputElement; +type Target = HTMLInputElement | InputObject; export interface FakeChangeEventHandler { target: Target; @@ -36,3 +36,11 @@ export function createFakeBlurEventHandler( type: "blur", }; } + +export function createFakeInputElement(obj: InputObject): HTMLInputElement { + const inputElement = document.createElement("input"); + inputElement.name = obj.name || ""; + inputElement.value = obj.value as string; + + return inputElement; +}