From 0ca916ec4b7533997aa8afd7ff8b553a86da80d5 Mon Sep 17 00:00:00 2001 From: Lars Briem Date: Wed, 10 Apr 2024 12:10:48 +0200 Subject: [PATCH 01/12] Store svz metadata in project properties --- OTAnalytics/adapter_ui/__init__.py | 0 OTAnalytics/adapter_ui/ui_texts.py | 6 + OTAnalytics/adapter_ui/view_model.py | 8 + OTAnalytics/application/application.py | 3 + OTAnalytics/application/datastore.py | 2 +- OTAnalytics/application/project.py | 19 +- .../application/use_cases/load_otconfig.py | 4 +- .../use_cases/reset_project_config.py | 2 +- .../application/use_cases/update_project.py | 16 +- .../customtkinter_gui/dummy_viewmodel.py | 7 + .../customtkinter_gui/frame_project.py | 166 +++++++++++++++++- tests/OTAnalytics/application/test_project.py | 10 +- .../use_cases/test_load_otconfig.py | 4 +- .../use_cases/test_reset_project_config.py | 2 +- .../use_cases/test_update_project.py | 38 +++- 15 files changed, 266 insertions(+), 21 deletions(-) create mode 100644 OTAnalytics/adapter_ui/__init__.py create mode 100644 OTAnalytics/adapter_ui/ui_texts.py diff --git a/OTAnalytics/adapter_ui/__init__.py b/OTAnalytics/adapter_ui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/OTAnalytics/adapter_ui/ui_texts.py b/OTAnalytics/adapter_ui/ui_texts.py new file mode 100644 index 000000000..d4ca90762 --- /dev/null +++ b/OTAnalytics/adapter_ui/ui_texts.py @@ -0,0 +1,6 @@ +from OTAnalytics.application.project import DirectionOfStationing + +DIRECTIONS_OF_STATIONING = { + DirectionOfStationing.IN_DIRECTION: "In Stationierungsrichtung", + DirectionOfStationing.OPPOSITE_DIRECTION: "Gegen Stationierungsrichtung", +} diff --git a/OTAnalytics/adapter_ui/view_model.py b/OTAnalytics/adapter_ui/view_model.py index c68870484..de1e8f146 100644 --- a/OTAnalytics/adapter_ui/view_model.py +++ b/OTAnalytics/adapter_ui/view_model.py @@ -17,6 +17,7 @@ from OTAnalytics.adapter_ui.abstract_frame_tracks import AbstractFrameTracks from OTAnalytics.adapter_ui.abstract_main_window import AbstractMainWindow from OTAnalytics.adapter_ui.abstract_treeview_interface import AbstractTreeviewInterface +from OTAnalytics.application.project import DirectionOfStationing from OTAnalytics.domain.date import DateRange from OTAnalytics.domain.flow import Flow from OTAnalytics.domain.section import Section @@ -389,3 +390,10 @@ def get_skip_frames(self) -> int: @abstractmethod def set_video_control_frame(self, frame: AbstractFrame) -> None: raise NotImplementedError + + @abstractmethod + def update_svz_metadata(self, metadata: dict) -> None: + raise NotImplementedError + + def get_directions_of_stationing(self) -> dict[DirectionOfStationing, str]: + raise NotImplementedError diff --git a/OTAnalytics/application/application.py b/OTAnalytics/application/application.py index b1a746a7e..b21761eac 100644 --- a/OTAnalytics/application/application.py +++ b/OTAnalytics/application/application.py @@ -613,6 +613,9 @@ def update_project_name(self, name: str) -> None: def update_project_start_date(self, start_date: datetime | None) -> None: self._project_updater.update_start_date(start_date) + def update_svz_metadata(self, metadata: dict) -> None: + self._project_updater.update_svz_metadata(metadata) + def get_track_repository_size(self) -> int: return self._track_repository_size.get() diff --git a/OTAnalytics/application/datastore.py b/OTAnalytics/application/datastore.py index 88713e20f..5dbfae87d 100644 --- a/OTAnalytics/application/datastore.py +++ b/OTAnalytics/application/datastore.py @@ -212,7 +212,7 @@ def __init__( self._video_repository = video_repository self._track_to_video_repository = track_to_video_repository self._progressbar = progressbar - self.project = Project(name="", start_date=None) + self.project = Project(name="", start_date=None, metadata={}) def register_video_observer(self, observer: VideoListObserver) -> None: self._video_repository.register_videos_observer(observer) diff --git a/OTAnalytics/application/project.py b/OTAnalytics/application/project.py index b6721af2d..89be6a4e6 100644 --- a/OTAnalytics/application/project.py +++ b/OTAnalytics/application/project.py @@ -1,18 +1,33 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from datetime import datetime +from enum import Enum from typing import Optional NAME: str = "name" START_DATE: str = "start_date" +METADATA: str = "metadata" +TK_NUMBER: str = "tk_number" +COUNTING_LOCATION_NUMBER: str = "counting_location_number" +DIRECTION: str = "direction" +REMARK: str = "remark" +COORDINATE_X: str = "coordinate_x" +COORDINATE_Y: str = "coordinate_y" + + +class DirectionOfStationing(Enum): + IN_DIRECTION = 1 + OPPOSITE_DIRECTION = 2 @dataclass class Project: name: str - start_date: Optional[datetime] + start_date: Optional[datetime] = None + metadata: Optional[dict] = field(default_factory=lambda: {}) def to_dict(self) -> dict: return { NAME: self.name, START_DATE: self.start_date.timestamp() if self.start_date else None, + METADATA: self.metadata if METADATA else None, } diff --git a/OTAnalytics/application/use_cases/load_otconfig.py b/OTAnalytics/application/use_cases/load_otconfig.py index c19bab81d..d52a8cf16 100644 --- a/OTAnalytics/application/use_cases/load_otconfig.py +++ b/OTAnalytics/application/use_cases/load_otconfig.py @@ -42,7 +42,9 @@ def load(self, file: Path) -> None: self._clear_repositories() config = self._config_parser.parse(file) try: - self._update_project(config.project.name, config.project.start_date) + self._update_project( + config.project.name, config.project.start_date, config.project.metadata + ) self._add_videos.add(config.videos) self._add_sections.add(config.sections) self._add_flows.add(config.flows) diff --git a/OTAnalytics/application/use_cases/reset_project_config.py b/OTAnalytics/application/use_cases/reset_project_config.py index 8b3ff4447..c65dbff37 100644 --- a/OTAnalytics/application/use_cases/reset_project_config.py +++ b/OTAnalytics/application/use_cases/reset_project_config.py @@ -13,4 +13,4 @@ def __init__(self, update_project: ProjectUpdater): def __call__(self) -> None: """Reset the project configuration.""" - self._update_project("", None) + self._update_project("", None, None) diff --git a/OTAnalytics/application/use_cases/update_project.py b/OTAnalytics/application/use_cases/update_project.py index d1b64a87b..e03e92fa6 100644 --- a/OTAnalytics/application/use_cases/update_project.py +++ b/OTAnalytics/application/use_cases/update_project.py @@ -13,22 +13,30 @@ def __init__(self, datastore: Datastore) -> None: self._datastore = datastore self._subject = Subject[Project]() - def __call__(self, name: str, start_date: Optional[datetime]) -> None: - project = Project(name=name, start_date=start_date) + def __call__( + self, name: str, start_date: Optional[datetime], metadata: Optional[dict] + ) -> None: + project = Project(name=name, start_date=start_date, metadata=metadata) self._datastore.project = project self._subject.notify(project) def update_name(self, name: str) -> None: old_project = self._datastore.project - new_project = Project(name, old_project.start_date) + new_project = Project(name, old_project.start_date, old_project.metadata) self._datastore.project = new_project self._subject.notify(new_project) def update_start_date(self, start_date: Optional[datetime]) -> None: old_project = self._datastore.project - new_project = Project(old_project.name, start_date) + new_project = Project(old_project.name, start_date, old_project.metadata) self._datastore.project = new_project self._subject.notify(new_project) def register(self, observer: OBSERVER[Project]) -> None: self._subject.register(observer) + + def update_svz_metadata(self, metadata: dict) -> None: + old_project = self._datastore.project + new_project = Project(old_project.name, old_project.start_date, metadata) + self._datastore.project = new_project + self._subject.notify(new_project) diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py b/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py index 0f929c36b..5b048a9f8 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py @@ -30,6 +30,7 @@ InnerSegmentsCenterCalculator, SectionRefPointCalculator, ) +from OTAnalytics.adapter_ui.ui_texts import DIRECTIONS_OF_STATIONING from OTAnalytics.adapter_ui.view_model import ( MetadataProvider, MissingCoordinate, @@ -1694,3 +1695,9 @@ def set_button_quick_save_config( self, button_quick_save_config: AbstractButtonQuickSaveConfig ) -> None: self._button_quick_save_config = button_quick_save_config + + def update_svz_metadata(self, metadata: dict) -> None: + self._application.update_svz_metadata(metadata) + + def get_directions_of_stationing(self) -> dict: + return DIRECTIONS_OF_STATIONING diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py b/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py index aeb323426..ab9407fab 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py @@ -3,10 +3,18 @@ from datetime import datetime from typing import Any, Optional -from customtkinter import CTkButton, CTkEntry, CTkLabel, ThemeManager +from customtkinter import CTkButton, CTkComboBox, CTkEntry, CTkLabel, ThemeManager from OTAnalytics.adapter_ui.abstract_frame_project import AbstractFrameProject from OTAnalytics.adapter_ui.view_model import ViewModel +from OTAnalytics.application.project import ( + COORDINATE_X, + COORDINATE_Y, + COUNTING_LOCATION_NUMBER, + DIRECTION, + REMARK, + TK_NUMBER, +) from OTAnalytics.plugin_ui.customtkinter_gui.button_quick_save_config import ( ButtonQuickSaveConfig, ) @@ -104,6 +112,7 @@ def _get_widgets(self) -> None: width=10, command=self._viewmodel.quick_save_configuration, ) + self._svz_metadata = TabviewSvzMetadata(master=self, viewmodel=self._viewmodel) def _place_widgets(self) -> None: self.grid_rowconfigure(2, weight=1) @@ -125,6 +134,9 @@ def _place_widgets(self) -> None: self._label_name.grid(row=1, column=0, padx=PADX, pady=PADY, sticky=STICKY) self._entry_name.grid(row=1, column=1, padx=PADX, pady=PADY, sticky=STICKY) self._start_date_row.grid(row=2, column=0, columnspan=2, sticky=STICKY_WEST) + self._svz_metadata.grid( + row=3, column=0, columnspan=2, padx=0, pady=0, sticky=STICKY + ) def _wire_callbacks(self) -> None: self._project_name.trace_add("write", callback=self._update_project_name) @@ -154,5 +166,157 @@ def set_enabled_general_buttons(self, enabled: bool) -> None: button.configure(state=new_state) +class TabviewSvzMetadata(CustomCTkTabview): + def __init__( + self, + viewmodel: ViewModel, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self._viewmodel = viewmodel + self._title: str = "SVZ Metadaten" + self._get_widgets() + self._place_widgets() + self.disable_segmented_button() + + def _get_widgets(self) -> None: + self.add(self._title) + self.frame_project = FrameSvzMetadata( + master=self.tab(self._title), viewmodel=self._viewmodel + ) + + def _place_widgets(self) -> None: + self.frame_project.pack(fill=tkinter.BOTH, expand=True) + self.set(self._title) + + +class FrameSvzMetadata(EmbeddedCTkFrame): + + def __init__(self, viewmodel: ViewModel, **kwargs: Any) -> None: + super().__init__(**kwargs) + self._viewmodel = viewmodel + self._tk_number = tkinter.StringVar() + self._counting_location_number = tkinter.StringVar() + self._coordinate_x = tkinter.StringVar() + self._coordinate_y = tkinter.StringVar() + self._direction = tkinter.StringVar() + self._remark = tkinter.StringVar() + self._get_widgets() + self._place_widgets() + self.introduce_to_viewmodel() + self._wire_callbacks() + + def _get_widgets(self) -> None: + self._label_tk_number = CTkLabel(master=self, text="TK-Nummer") + self._entry_tk_number = CTkEntry( + master=self, + textvariable=self._tk_number, + placeholder_text="TK-Nummer", + ) + self._label_counting_location_number = CTkLabel( + master=self, text="Zählstellennummer" + ) + self._entry_counting_location_number = CTkEntry( + master=self, + textvariable=self._counting_location_number, + placeholder_text="Zählstellennummer", + ) + self._label_coordinate = CTkLabel(master=self, text="Geokoordinate") + self._label_coordinate_x = CTkLabel(master=self, text="X") + self._entry_coordinate_x = CTkEntry( + master=self, + textvariable=self._coordinate_x, + placeholder_text="X Koordinate", + ) + self._label_coordinate_y = CTkLabel(master=self, text="Y") + self._entry_coordinate_y = CTkEntry( + master=self, + textvariable=self._coordinate_y, + placeholder_text="Y Koordinate", + ) + self._label_direction = CTkLabel(master=self, text="Ausrichtung") + self._entry_direction = CTkComboBox( + master=self, + variable=self._direction, + values=list(self._viewmodel.get_directions_of_stationing().values()), + ) + self._label_remark = CTkLabel(master=self, text="Bemerkung") + self._entry_remark = CTkEntry( + master=self, + textvariable=self._remark, + placeholder_text="Bemerkung", + ) + + def introduce_to_viewmodel(self) -> None: + pass + + def _place_widgets(self) -> None: + self.grid_rowconfigure((0, 1, 2, 3, 4), weight=1) + self.grid_columnconfigure(0, weight=0) + self.grid_columnconfigure(1, weight=1) + self._label_tk_number.grid( + row=0, column=0, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY_WEST + ) + self._entry_tk_number.grid( + row=0, column=1, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY + ) + self._label_counting_location_number.grid( + row=1, column=0, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY_WEST + ) + self._entry_counting_location_number.grid( + row=1, column=1, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY + ) + self._label_direction.grid( + row=2, column=0, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY_WEST + ) + self._entry_direction.grid( + row=2, column=1, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY + ) + self._label_remark.grid( + row=3, column=0, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY_WEST + ) + self._entry_remark.grid( + row=3, column=1, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY + ) + self._label_coordinate.grid( + row=4, column=0, columnspan=2, padx=PADX, pady=PADY, sticky=STICKY + ) + self._label_coordinate_x.grid( + row=5, column=0, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY + ) + self._entry_coordinate_x.grid( + row=5, column=1, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY + ) + self._label_coordinate_y.grid( + row=6, column=0, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY + ) + self._entry_coordinate_y.grid( + row=6, column=1, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY + ) + + def _wire_callbacks(self) -> None: + self._tk_number.trace_add("write", callback=self._update_metadata) + self._counting_location_number.trace_add( + "write", callback=self._update_metadata + ) + self._direction.trace_add("write", callback=self._update_metadata) + self._remark.trace_add("write", callback=self._update_metadata) + self._coordinate_x.trace_add("write", callback=self._update_metadata) + self._coordinate_y.trace_add("write", callback=self._update_metadata) + + def _update_metadata(self, name: str, other: str, mode: str) -> None: + self._viewmodel.update_svz_metadata(self.__build_metadata()) + + def __build_metadata(self) -> dict: + return { + TK_NUMBER: self._tk_number.get(), + COUNTING_LOCATION_NUMBER: self._counting_location_number.get(), + DIRECTION: self._direction.get(), + REMARK: self._remark.get(), + COORDINATE_X: self._coordinate_x.get(), + COORDINATE_Y: self._coordinate_y.get(), + } + + def get_default_toplevel_fg_color() -> str: return ThemeManager.theme["CTkToplevel"]["fg_color"] diff --git a/tests/OTAnalytics/application/test_project.py b/tests/OTAnalytics/application/test_project.py index e97f009fc..2489915b0 100644 --- a/tests/OTAnalytics/application/test_project.py +++ b/tests/OTAnalytics/application/test_project.py @@ -1,13 +1,17 @@ from datetime import datetime -from OTAnalytics.application.project import NAME, START_DATE, Project +from OTAnalytics.application.project import METADATA, NAME, START_DATE, Project class TestProject: def test_to_dict(self) -> None: + name = "some" timestamp = 12345678 - project = Project(name="some", start_date=datetime.fromtimestamp(timestamp)) + metadata = {"metadata": "svz"} + project = Project( + name=name, start_date=datetime.fromtimestamp(timestamp), metadata=metadata + ) result = project.to_dict() - assert result == {NAME: "some", START_DATE: timestamp} + assert result == {NAME: name, START_DATE: timestamp, METADATA: metadata} diff --git a/tests/OTAnalytics/application/use_cases/test_load_otconfig.py b/tests/OTAnalytics/application/use_cases/test_load_otconfig.py index cd9ee3a04..efd878091 100644 --- a/tests/OTAnalytics/application/use_cases/test_load_otconfig.py +++ b/tests/OTAnalytics/application/use_cases/test_load_otconfig.py @@ -55,7 +55,9 @@ def test_load(self, otconfig: OtConfig) -> None: load_otconfig.load(file) update_project.assert_called_once_with( - otconfig.project.name, otconfig.project.start_date + otconfig.project.name, + otconfig.project.start_date, + otconfig.project.metadata, ) add_videos.add.assert_called_once_with(otconfig.videos) add_sections.add.assert_called_once_with(otconfig.sections) diff --git a/tests/OTAnalytics/application/use_cases/test_reset_project_config.py b/tests/OTAnalytics/application/use_cases/test_reset_project_config.py index d01ef79b5..f36457ccf 100644 --- a/tests/OTAnalytics/application/use_cases/test_reset_project_config.py +++ b/tests/OTAnalytics/application/use_cases/test_reset_project_config.py @@ -9,4 +9,4 @@ def test_reset(self) -> None: update_project = Mock(spec=ProjectUpdater) reset_project_config = ResetProjectConfig(update_project) reset_project_config() - assert update_project.call_args_list == [call("", None)] + assert update_project.call_args_list == [call("", None, None)] diff --git a/tests/OTAnalytics/application/use_cases/test_update_project.py b/tests/OTAnalytics/application/use_cases/test_update_project.py index aef5a612b..b13ad7032 100644 --- a/tests/OTAnalytics/application/use_cases/test_update_project.py +++ b/tests/OTAnalytics/application/use_cases/test_update_project.py @@ -10,7 +10,7 @@ @pytest.fixture def my_project() -> Project: - return Project("My Project", datetime(2022, 1, 1, 13)) + return Project("My Project", datetime(2022, 1, 1, 13), {"metadata": "default data"}) @pytest.fixture @@ -20,16 +20,25 @@ def datastore(my_project: Project) -> Mock: return datastore +@pytest.fixture +def project_metadata() -> dict: + return {"new": "project metadata"} + + class TestUpdateProject: - def test_update(self, datastore: Mock, my_project: Project) -> None: + def test_update( + self, datastore: Mock, my_project: Project, project_metadata: dict + ) -> None: new_project_name = "My New Project" new_project_start_date = datetime(2000, 2, 2, 13) project_updater = ProjectUpdater(datastore) observer = Mock() project_updater.register(observer) - project_updater(new_project_name, new_project_start_date) - expected_project = Project(new_project_name, new_project_start_date) + project_updater(new_project_name, new_project_start_date, project_metadata) + expected_project = Project( + new_project_name, new_project_start_date, project_metadata + ) assert datastore.project == expected_project observer.assert_called_once_with(expected_project) @@ -41,7 +50,9 @@ def test_update_name(self, datastore: Mock, my_project: Project) -> None: project_updater.register(observer) project_updater.update_name(new_project_name) - expected_project = Project(new_project_name, my_project.start_date) + expected_project = Project( + new_project_name, my_project.start_date, my_project.metadata + ) assert datastore.project == expected_project observer.assert_called_once_with(expected_project) @@ -53,7 +64,22 @@ def test_update_start_date(self, datastore: Mock, my_project: Project) -> None: project_updater.register(observer) project_updater.update_start_date(new_project_start_date) - expected_project = Project(my_project.name, new_project_start_date) + expected_project = Project( + my_project.name, new_project_start_date, my_project.metadata + ) + + assert datastore.project == expected_project + observer.assert_called_once_with(expected_project) + + def test_update_svz_metadata( + self, datastore: Mock, my_project: Project, project_metadata: dict + ) -> None: + new_metadata = {"svz": "new metadata"} + project_updater = ProjectUpdater(datastore) + observer = Mock() + project_updater.register(observer) + project_updater.update_svz_metadata(new_metadata) + expected_project = Project(my_project.name, my_project.start_date, new_metadata) assert datastore.project == expected_project observer.assert_called_once_with(expected_project) From c7e74c0f4d497cc715eab30fef7ee5b3fde4362e Mon Sep 17 00:00:00 2001 From: Lars Briem Date: Wed, 10 Apr 2024 12:21:07 +0200 Subject: [PATCH 02/12] Refactor text resource handling --- .../customtkinter_gui/toplevel_flows.py | 65 ++++++++++--------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/toplevel_flows.py b/OTAnalytics/plugin_ui/customtkinter_gui/toplevel_flows.py index 493892ba4..9f41d6287 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/toplevel_flows.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/toplevel_flows.py @@ -38,6 +38,34 @@ class InvalidFlowNameException(Exception): pass +class TextResources: + def __init__(self, resources: list[ColumnResource]) -> None: + self._resources = resources + self._to_id = self._create_to_id(resources) + self._to_name = self._create_to_name(resources) + + @staticmethod + def _create_to_id(sections: list[ColumnResource]) -> dict[str, str]: + return {resource.values[COLUMN_SECTION]: resource.id for resource in sections} + + @staticmethod + def _create_to_name(sections: list[ColumnResource]) -> dict[str, str]: + return {resource.id: resource.values[COLUMN_SECTION] for resource in sections} + + @property + def names(self) -> list[str]: + return [resource.values[COLUMN_SECTION] for resource in self._resources] + + def get_name_for(self, resource_id: str) -> str: + return self._to_name.get(resource_id, "") + + def get_id_for(self, name: str) -> str: + return self._to_id.get(name, "") + + def has(self, resource_id: str) -> bool: + return resource_id in [resource.id for resource in self._resources] + + class FrameConfigureFlow(FrameContent): def __init__( self, @@ -48,10 +76,8 @@ def __init__( **kwargs: Any, ) -> None: super().__init__(**kwargs) - self._section_ids = section_ids + self._section_ids = TextResources(section_ids) self._name_generator = name_generator - self._section_name_to_id = self._create_section_name_to_id(section_ids) - self._section_id_to_name = self._create_section_id_to_name(section_ids) self._current_name = StringVar() self._input_values: dict = self.__create_input_values(input_values) self._show_distance = show_distance @@ -68,7 +94,7 @@ def _get_widgets(self) -> None: self.dropdown_section_start = CTkOptionMenu( master=self, width=180, - values=self._section_names(), + values=self._section_ids.names, command=self._autofill_name, ) self.dropdown_section_start.set(self._get_start_section_name()) @@ -76,7 +102,7 @@ def _get_widgets(self) -> None: self.dropdown_section_end = CTkOptionMenu( master=self, width=180, - values=self._section_names(), + values=self._section_ids.names, command=self._autofill_name, ) self.dropdown_section_end.set(self._get_end_section_name()) @@ -109,16 +135,6 @@ def _place_widgets(self) -> None: self.label_distance.grid(row=3, column=0, padx=PADX, pady=PADY, sticky=E) self.entry_distance.grid(row=3, column=1, padx=PADX, pady=PADY, sticky=W) - def _create_section_name_to_id( - self, sections: list[ColumnResource] - ) -> dict[str, str]: - return {resource.values[COLUMN_SECTION]: resource.id for resource in sections} - - def _create_section_id_to_name( - self, sections: list[ColumnResource] - ) -> dict[str, str]: - return {resource.id: resource.values[COLUMN_SECTION] for resource in sections} - def __set_initial_values(self) -> None: self._current_name.set(self._input_values.get(FLOW_NAME, "")) @@ -133,9 +149,6 @@ def __create_input_values(self, input_values: Optional[dict]) -> dict: DISTANCE: None, } - def _section_names(self) -> list[str]: - return [resource.values[COLUMN_SECTION] for resource in self._section_ids] - def _autofill_name(self, event: Any) -> None: if self._last_autofilled_name == self.entry_name.get(): self.entry_name.delete(0, tkinter.END) @@ -149,25 +162,19 @@ def _autofill_name(self, event: Any) -> None: def _get_end_section_name(self) -> str: _id = self._input_values[END_SECTION] - return self._get_section_name_for_id(_id) - - def _get_section_name_for_id(self, name: str) -> str: - return self._section_id_to_name.get(name, "") + return self._section_ids.get_name_for(_id) def _get_start_section_name(self) -> str: _id = self._input_values[START_SECTION] - return self._get_section_name_for_id(_id) + return self._section_ids.get_name_for(_id) def _get_end_section_id(self) -> str: name = self.dropdown_section_end.get() - return self._get_section_id_for_name(name) - - def _get_section_id_for_name(self, name: str) -> str: - return self._section_name_to_id.get(name, "") + return self._section_ids.get_id_for(name) def _get_start_section_id(self) -> str: name = self.dropdown_section_start.get() - return self._get_section_id_for_name(name) + return self._section_ids.get_id_for(name) def _is_float_above_zero(self, entry_value: Any) -> bool: try: @@ -193,7 +200,7 @@ def _check_sections(self) -> None: ) else: for section in sections: - if section not in [resource.id for resource in self._section_ids]: + if not self._section_ids.has(section): raise NotExistingSectionException( f"{section} is not an existing section" ) From 2f11eca98bdb023e7453f69568b0dab5e75354b4 Mon Sep 17 00:00:00 2001 From: Lars Briem Date: Wed, 10 Apr 2024 12:27:36 +0200 Subject: [PATCH 03/12] Refactor text resource handling --- OTAnalytics/adapter_ui/text_resources.py | 42 +++++++++++++++++++ .../customtkinter_gui/dummy_viewmodel.py | 5 +-- .../customtkinter_gui/frame_files.py | 6 +-- .../customtkinter_gui/frame_flows.py | 6 +-- .../customtkinter_gui/frame_sections.py | 14 +++---- .../customtkinter_gui/frame_videos.py | 6 +-- .../customtkinter_gui/toplevel_flows.py | 31 +------------- .../customtkinter_gui/treeview_template.py | 13 +----- 8 files changed, 57 insertions(+), 66 deletions(-) create mode 100644 OTAnalytics/adapter_ui/text_resources.py diff --git a/OTAnalytics/adapter_ui/text_resources.py b/OTAnalytics/adapter_ui/text_resources.py new file mode 100644 index 000000000..082f9395b --- /dev/null +++ b/OTAnalytics/adapter_ui/text_resources.py @@ -0,0 +1,42 @@ +from dataclasses import dataclass + +COLUMN_NAME = "column_name" + + +@dataclass(frozen=True, order=True) +class ColumnResource: + """ + Represents a row in a treeview with an id and a dict of values to be shown. + The dicts keys represent the columns and the values represent the cell values. + """ + + id: str + values: dict[str, str] + + +class TextResources: + def __init__(self, resources: list[ColumnResource]) -> None: + self._resources = resources + self._to_id = self._create_to_id(resources) + self._to_name = self._create_to_name(resources) + + @staticmethod + def _create_to_id(sections: list[ColumnResource]) -> dict[str, str]: + return {resource.values[COLUMN_NAME]: resource.id for resource in sections} + + @staticmethod + def _create_to_name(sections: list[ColumnResource]) -> dict[str, str]: + return {resource.id: resource.values[COLUMN_NAME] for resource in sections} + + @property + def names(self) -> list[str]: + return [resource.values[COLUMN_NAME] for resource in self._resources] + + def get_name_for(self, resource_id: str) -> str: + return self._to_name.get(resource_id, "") + + def get_id_for(self, name: str) -> str: + return self._to_id.get(name, "") + + def has(self, resource_id: str) -> bool: + return resource_id in [resource.id for resource in self._resources] diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py b/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py index 5b048a9f8..bc3cc708d 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py @@ -30,6 +30,7 @@ InnerSegmentsCenterCalculator, SectionRefPointCalculator, ) +from OTAnalytics.adapter_ui.text_resources import COLUMN_NAME, ColumnResource from OTAnalytics.adapter_ui.ui_texts import DIRECTIONS_OF_STATIONING from OTAnalytics.adapter_ui.view_model import ( MetadataProvider, @@ -94,7 +95,6 @@ from OTAnalytics.domain.types import EventType from OTAnalytics.domain.video import DifferentDrivesException, Video, VideoListObserver from OTAnalytics.plugin_ui.customtkinter_gui import toplevel_export_events -from OTAnalytics.plugin_ui.customtkinter_gui.frame_sections import COLUMN_SECTION from OTAnalytics.plugin_ui.customtkinter_gui.helpers import ask_for_save_file_path from OTAnalytics.plugin_ui.customtkinter_gui.line_section import ( ArrowPainter, @@ -135,7 +135,6 @@ ToplevelFlows, ) from OTAnalytics.plugin_ui.customtkinter_gui.toplevel_sections import ToplevelSections -from OTAnalytics.plugin_ui.customtkinter_gui.treeview_template import ColumnResource MESSAGE_CONFIGURATION_NOT_SAVED = "The configuration has not been saved.\n" SUPPORTED_VIDEO_FILE_TYPES = ["*.avi", "*.mkv", "*.mov", "*.mp4"] @@ -1250,7 +1249,7 @@ def generate_flows(self) -> None: self._application.generate_flows() def __to_resource(self, section: Section) -> ColumnResource: - values = {COLUMN_SECTION: section.name} + values = {COLUMN_NAME: section.name} return ColumnResource(id=section.id.serialize(), values=values) def __update_flow_data(self, flow_data: dict) -> None: diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/frame_files.py b/OTAnalytics/plugin_ui/customtkinter_gui/frame_files.py index 24253f945..531187443 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/frame_files.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/frame_files.py @@ -4,13 +4,11 @@ from customtkinter import CTkButton, CTkFrame, CTkScrollbar +from OTAnalytics.adapter_ui.text_resources import ColumnResource from OTAnalytics.adapter_ui.view_model import ViewModel from OTAnalytics.plugin_ui.customtkinter_gui.constants import PADX, PADY, STICKY from OTAnalytics.plugin_ui.customtkinter_gui.custom_containers import EmbeddedCTkFrame -from OTAnalytics.plugin_ui.customtkinter_gui.treeview_template import ( - ColumnResource, - TreeviewTemplate, -) +from OTAnalytics.plugin_ui.customtkinter_gui.treeview_template import TreeviewTemplate class FrameFiles(EmbeddedCTkFrame): diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/frame_flows.py b/OTAnalytics/plugin_ui/customtkinter_gui/frame_flows.py index f5774e165..a37d8ca2d 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/frame_flows.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/frame_flows.py @@ -3,14 +3,12 @@ from customtkinter import CTkButton, CTkFrame, CTkScrollbar +from OTAnalytics.adapter_ui.text_resources import ColumnResource from OTAnalytics.adapter_ui.view_model import ViewModel from OTAnalytics.domain.flow import Flow from OTAnalytics.plugin_ui.customtkinter_gui.abstract_ctk_frame import AbstractCTkFrame from OTAnalytics.plugin_ui.customtkinter_gui.constants import PADX, PADY, STICKY -from OTAnalytics.plugin_ui.customtkinter_gui.treeview_template import ( - ColumnResource, - TreeviewTemplate, -) +from OTAnalytics.plugin_ui.customtkinter_gui.treeview_template import TreeviewTemplate class FrameFlows(AbstractCTkFrame): diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/frame_sections.py b/OTAnalytics/plugin_ui/customtkinter_gui/frame_sections.py index d40d65702..79d024813 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/frame_sections.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/frame_sections.py @@ -4,16 +4,12 @@ from customtkinter import CTkButton, CTkFrame, CTkScrollbar +from OTAnalytics.adapter_ui.text_resources import COLUMN_NAME, ColumnResource from OTAnalytics.adapter_ui.view_model import ViewModel from OTAnalytics.domain.section import Section from OTAnalytics.plugin_ui.customtkinter_gui.abstract_ctk_frame import AbstractCTkFrame from OTAnalytics.plugin_ui.customtkinter_gui.constants import PADX, PADY, STICKY -from OTAnalytics.plugin_ui.customtkinter_gui.treeview_template import ( - ColumnResource, - TreeviewTemplate, -) - -COLUMN_SECTION = "Section" +from OTAnalytics.plugin_ui.customtkinter_gui.treeview_template import TreeviewTemplate class FrameSections(AbstractCTkFrame): @@ -125,10 +121,10 @@ def __init__(self, viewmodel: ViewModel, **kwargs: Any) -> None: self.update_items() def _define_columns(self) -> None: - columns = [COLUMN_SECTION] + columns = [COLUMN_NAME] self["columns"] = columns self.column(column="#0", width=0, stretch=False) - self.column(column=COLUMN_SECTION, anchor="center", width=150, minwidth=40) + self.column(column=COLUMN_NAME, anchor="center", width=150, minwidth=40) self["displaycolumns"] = columns def _introduce_to_viewmodel(self) -> None: @@ -149,7 +145,7 @@ def update_items(self) -> None: self.add_items(item_ids=sorted(item_ids)) def __to_resource(self, section: Section) -> ColumnResource: - values = {COLUMN_SECTION: section.name} + values = {COLUMN_NAME: section.name} return ColumnResource(id=section.id.id, values=values) diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/frame_videos.py b/OTAnalytics/plugin_ui/customtkinter_gui/frame_videos.py index ef4b43334..1529c3276 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/frame_videos.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/frame_videos.py @@ -3,14 +3,12 @@ from customtkinter import CTkButton, CTkFrame, CTkScrollbar +from OTAnalytics.adapter_ui.text_resources import ColumnResource from OTAnalytics.adapter_ui.view_model import ViewModel from OTAnalytics.domain.video import Video from OTAnalytics.plugin_ui.customtkinter_gui.abstract_ctk_frame import AbstractCTkFrame from OTAnalytics.plugin_ui.customtkinter_gui.constants import PADX, PADY, STICKY -from OTAnalytics.plugin_ui.customtkinter_gui.treeview_template import ( - ColumnResource, - TreeviewTemplate, -) +from OTAnalytics.plugin_ui.customtkinter_gui.treeview_template import TreeviewTemplate class FrameVideos(AbstractCTkFrame): diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/toplevel_flows.py b/OTAnalytics/plugin_ui/customtkinter_gui/toplevel_flows.py index 9f41d6287..9c72ffe65 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/toplevel_flows.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/toplevel_flows.py @@ -4,16 +4,15 @@ from customtkinter import CTkEntry, CTkLabel, CTkOptionMenu +from OTAnalytics.adapter_ui.text_resources import ColumnResource, TextResources from OTAnalytics.application.application import CancelAddFlow from OTAnalytics.application.logger import logger from OTAnalytics.application.use_cases.generate_flows import FlowNameGenerator from OTAnalytics.plugin_ui.customtkinter_gui.constants import PADX, PADY -from OTAnalytics.plugin_ui.customtkinter_gui.frame_sections import COLUMN_SECTION from OTAnalytics.plugin_ui.customtkinter_gui.toplevel_template import ( FrameContent, ToplevelTemplate, ) -from OTAnalytics.plugin_ui.customtkinter_gui.treeview_template import ColumnResource FLOW_ID = "Id" FLOW_NAME = "Name" @@ -38,34 +37,6 @@ class InvalidFlowNameException(Exception): pass -class TextResources: - def __init__(self, resources: list[ColumnResource]) -> None: - self._resources = resources - self._to_id = self._create_to_id(resources) - self._to_name = self._create_to_name(resources) - - @staticmethod - def _create_to_id(sections: list[ColumnResource]) -> dict[str, str]: - return {resource.values[COLUMN_SECTION]: resource.id for resource in sections} - - @staticmethod - def _create_to_name(sections: list[ColumnResource]) -> dict[str, str]: - return {resource.id: resource.values[COLUMN_SECTION] for resource in sections} - - @property - def names(self) -> list[str]: - return [resource.values[COLUMN_SECTION] for resource in self._resources] - - def get_name_for(self, resource_id: str) -> str: - return self._to_name.get(resource_id, "") - - def get_id_for(self, name: str) -> str: - return self._to_id.get(name, "") - - def has(self, resource_id: str) -> bool: - return resource_id in [resource.id for resource in self._resources] - - class FrameConfigureFlow(FrameContent): def __init__( self, diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/treeview_template.py b/OTAnalytics/plugin_ui/customtkinter_gui/treeview_template.py index a54c4eca5..741df4628 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/treeview_template.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/treeview_template.py @@ -1,26 +1,15 @@ from abc import abstractmethod -from dataclasses import dataclass from tkinter.ttk import Treeview from typing import Any, Literal from OTAnalytics.adapter_ui.abstract_treeview_interface import AbstractTreeviewInterface +from OTAnalytics.adapter_ui.text_resources import ColumnResource from OTAnalytics.plugin_ui.customtkinter_gui.constants import tk_events from OTAnalytics.plugin_ui.customtkinter_gui.helpers import get_widget_position EMPTY_SELECTION: list[str] = [] -@dataclass(frozen=True, order=True) -class ColumnResource: - """ - Represents a row in a treeview with an id and a dict of values to be shown. - The dicts keys represent the columns and the values represent the cell values. - """ - - id: str - values: dict[str, str] - - class TreeviewTemplate(AbstractTreeviewInterface, Treeview): def __init__( self, From 5fdfc4331bacf8e0b50c6592de7d6dcaa2620ced Mon Sep 17 00:00:00 2001 From: Lars Briem Date: Wed, 10 Apr 2024 12:33:26 +0200 Subject: [PATCH 04/12] Refactor text resource handling --- OTAnalytics/adapter_ui/text_resources.py | 2 +- .../customtkinter_gui/dummy_viewmodel.py | 19 ++++++++++++------- .../customtkinter_gui/toplevel_flows.py | 8 ++++---- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/OTAnalytics/adapter_ui/text_resources.py b/OTAnalytics/adapter_ui/text_resources.py index 082f9395b..06427e4b7 100644 --- a/OTAnalytics/adapter_ui/text_resources.py +++ b/OTAnalytics/adapter_ui/text_resources.py @@ -14,7 +14,7 @@ class ColumnResource: values: dict[str, str] -class TextResources: +class ColumnResources: def __init__(self, resources: list[ColumnResource]) -> None: self._resources = resources self._to_id = self._create_to_id(resources) diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py b/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py index bc3cc708d..293b3e9b0 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py @@ -30,7 +30,11 @@ InnerSegmentsCenterCalculator, SectionRefPointCalculator, ) -from OTAnalytics.adapter_ui.text_resources import COLUMN_NAME, ColumnResource +from OTAnalytics.adapter_ui.text_resources import ( + COLUMN_NAME, + ColumnResource, + ColumnResources, +) from OTAnalytics.adapter_ui.ui_texts import DIRECTIONS_OF_STATIONING from OTAnalytics.adapter_ui.view_model import ( MetadataProvider, @@ -1178,15 +1182,16 @@ def _show_flow_popup( if self._treeview_flows is None: raise MissingInjectedInstanceError(type(self._treeview_flows).__name__) position = self._treeview_flows.get_position() - section_ids = [ - self.__to_resource(section) for section in self.get_all_sections() - ] - if len(section_ids) < 2: + sections = list(self.get_all_sections()) + if len(sections) < 2: InfoBox( message="To add a flow, at least two sections are needed", initial_position=position, ) raise CancelAddFlow() + section_ids = ColumnResources( + [self.__to_resource(section) for section in sections] + ) return self.__create_flow_data(input_values, title, position, section_ids) def __create_flow_data( @@ -1194,7 +1199,7 @@ def __create_flow_data( input_values: dict | None, title: str, position: tuple[int, int], - section_ids: list[ColumnResource], + section_ids: ColumnResources, ) -> dict: flow_data = self.__get_flow_data(input_values, title, position, section_ids) while (not flow_data) or not (self.__is_flow_name_valid(flow_data)): @@ -1221,7 +1226,7 @@ def __get_flow_data( input_values: dict | None, title: str, position: tuple[int, int], - section_ids: list[ColumnResource], + section_ids: ColumnResources, ) -> dict: return ToplevelFlows( title=title, diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/toplevel_flows.py b/OTAnalytics/plugin_ui/customtkinter_gui/toplevel_flows.py index 9c72ffe65..391280b60 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/toplevel_flows.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/toplevel_flows.py @@ -4,7 +4,7 @@ from customtkinter import CTkEntry, CTkLabel, CTkOptionMenu -from OTAnalytics.adapter_ui.text_resources import ColumnResource, TextResources +from OTAnalytics.adapter_ui.text_resources import ColumnResources from OTAnalytics.application.application import CancelAddFlow from OTAnalytics.application.logger import logger from OTAnalytics.application.use_cases.generate_flows import FlowNameGenerator @@ -40,14 +40,14 @@ class InvalidFlowNameException(Exception): class FrameConfigureFlow(FrameContent): def __init__( self, - section_ids: list[ColumnResource], + section_ids: ColumnResources, name_generator: FlowNameGenerator, input_values: dict | None = None, show_distance: bool = True, **kwargs: Any, ) -> None: super().__init__(**kwargs) - self._section_ids = TextResources(section_ids) + self._section_ids = section_ids self._name_generator = name_generator self._current_name = StringVar() self._input_values: dict = self.__create_input_values(input_values) @@ -193,7 +193,7 @@ def get_input_values(self) -> dict: class ToplevelFlows(ToplevelTemplate): def __init__( self, - section_ids: list[ColumnResource], + section_ids: ColumnResources, name_generator: FlowNameGenerator, input_values: dict | None = None, show_distance: bool = True, From 9e1948d95a3cde955c7a6edbe097d0b8cd982223 Mon Sep 17 00:00:00 2001 From: Lars Briem Date: Wed, 10 Apr 2024 12:42:12 +0200 Subject: [PATCH 05/12] Refactor text resource handling --- OTAnalytics/adapter_ui/text_resources.py | 4 ++++ .../customtkinter_gui/frame_files.py | 15 +++++++----- .../customtkinter_gui/frame_flows.py | 15 +++++++----- .../customtkinter_gui/frame_sections.py | 23 +++++++++++++------ .../customtkinter_gui/frame_videos.py | 11 +++++---- .../customtkinter_gui/treeview_template.py | 4 ++-- 6 files changed, 46 insertions(+), 26 deletions(-) diff --git a/OTAnalytics/adapter_ui/text_resources.py b/OTAnalytics/adapter_ui/text_resources.py index 06427e4b7..931401256 100644 --- a/OTAnalytics/adapter_ui/text_resources.py +++ b/OTAnalytics/adapter_ui/text_resources.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import Iterator COLUMN_NAME = "column_name" @@ -40,3 +41,6 @@ def get_id_for(self, name: str) -> str: def has(self, resource_id: str) -> bool: return resource_id in [resource.id for resource in self._resources] + + def __iter__(self) -> Iterator[ColumnResource]: + return self._resources.__iter__() diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/frame_files.py b/OTAnalytics/plugin_ui/customtkinter_gui/frame_files.py index 531187443..1ef0cc0b5 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/frame_files.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/frame_files.py @@ -4,7 +4,7 @@ from customtkinter import CTkButton, CTkFrame, CTkScrollbar -from OTAnalytics.adapter_ui.text_resources import ColumnResource +from OTAnalytics.adapter_ui.text_resources import ColumnResource, ColumnResources from OTAnalytics.adapter_ui.view_model import ViewModel from OTAnalytics.plugin_ui.customtkinter_gui.constants import PADX, PADY, STICKY from OTAnalytics.plugin_ui.customtkinter_gui.custom_containers import EmbeddedCTkFrame @@ -103,14 +103,17 @@ def update_items(self) -> None: else: track_files_have_videos.append(False) - item_ids = [ - self.__to_resource(file=file, video_loaded=video_loaded) - for file, video_loaded in zip(track_files, track_files_have_videos) - ] + item_ids = ColumnResources( + [ + self.__to_resource(file=file, video_loaded=video_loaded) + for file, video_loaded in zip(track_files, track_files_have_videos) + ] + ) self.add_items(item_ids=item_ids) + @staticmethod def __to_resource( - self, file: Path, video_loaded: bool, tracks_loaded: bool = True + file: Path, video_loaded: bool, tracks_loaded: bool = True ) -> ColumnResource: values = { COLUMN_FILE: file.stem, diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/frame_flows.py b/OTAnalytics/plugin_ui/customtkinter_gui/frame_flows.py index a37d8ca2d..c5c4c1f05 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/frame_flows.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/frame_flows.py @@ -3,7 +3,7 @@ from customtkinter import CTkButton, CTkFrame, CTkScrollbar -from OTAnalytics.adapter_ui.text_resources import ColumnResource +from OTAnalytics.adapter_ui.text_resources import ColumnResource, ColumnResources from OTAnalytics.adapter_ui.view_model import ViewModel from OTAnalytics.domain.flow import Flow from OTAnalytics.plugin_ui.customtkinter_gui.abstract_ctk_frame import AbstractCTkFrame @@ -129,11 +129,14 @@ def _on_double_click(self, event: Any) -> None: def update_items(self) -> None: self.delete(*self.get_children()) - item_ids = [ - self.__to_resource(flow) for flow in self._viewmodel.get_all_flows() - ] - self.add_items(item_ids=sorted(item_ids)) + item_ids = ColumnResources( + sorted( + [self.__to_resource(flow) for flow in self._viewmodel.get_all_flows()] + ) + ) + self.add_items(item_ids=item_ids) - def __to_resource(self, flow: Flow) -> ColumnResource: + @staticmethod + def __to_resource(flow: Flow) -> ColumnResource: values = {COLUMN_FLOW: flow.name} return ColumnResource(id=flow.id.id, values=values) diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/frame_sections.py b/OTAnalytics/plugin_ui/customtkinter_gui/frame_sections.py index 79d024813..d495c061f 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/frame_sections.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/frame_sections.py @@ -4,7 +4,11 @@ from customtkinter import CTkButton, CTkFrame, CTkScrollbar -from OTAnalytics.adapter_ui.text_resources import COLUMN_NAME, ColumnResource +from OTAnalytics.adapter_ui.text_resources import ( + COLUMN_NAME, + ColumnResource, + ColumnResources, +) from OTAnalytics.adapter_ui.view_model import ViewModel from OTAnalytics.domain.section import Section from OTAnalytics.plugin_ui.customtkinter_gui.abstract_ctk_frame import AbstractCTkFrame @@ -138,13 +142,18 @@ def _on_double_click(self, event: Any) -> None: def update_items(self) -> None: self.delete(*self.get_children()) - item_ids = [ - self.__to_resource(section) - for section in self._viewmodel.get_all_sections() - ] - self.add_items(item_ids=sorted(item_ids)) + item_ids = ColumnResources( + sorted( + [ + self.__to_resource(section) + for section in self._viewmodel.get_all_sections() + ] + ) + ) + self.add_items(item_ids=item_ids) - def __to_resource(self, section: Section) -> ColumnResource: + @staticmethod + def __to_resource(section: Section) -> ColumnResource: values = {COLUMN_NAME: section.name} return ColumnResource(id=section.id.id, values=values) diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/frame_videos.py b/OTAnalytics/plugin_ui/customtkinter_gui/frame_videos.py index 1529c3276..0cb9eb734 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/frame_videos.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/frame_videos.py @@ -3,7 +3,7 @@ from customtkinter import CTkButton, CTkFrame, CTkScrollbar -from OTAnalytics.adapter_ui.text_resources import ColumnResource +from OTAnalytics.adapter_ui.text_resources import ColumnResource, ColumnResources from OTAnalytics.adapter_ui.view_model import ViewModel from OTAnalytics.domain.video import Video from OTAnalytics.plugin_ui.customtkinter_gui.abstract_ctk_frame import AbstractCTkFrame @@ -95,11 +95,12 @@ def _on_double_click(self, event: Any) -> None: def update_items(self) -> None: self.delete(*self.get_children()) - item_ids = [ - self.__to_resource(video) for video in self._viewmodel.get_all_videos() - ] + item_ids = ColumnResources( + [self.__to_resource(video) for video in self._viewmodel.get_all_videos()] + ) self.add_items(item_ids=item_ids) - def __to_resource(self, video: Video) -> ColumnResource: + @staticmethod + def __to_resource(video: Video) -> ColumnResource: values = {COLUMN_VIDEO: video.get_path().name} return ColumnResource(id=str(video.get_path()), values=values) diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/treeview_template.py b/OTAnalytics/plugin_ui/customtkinter_gui/treeview_template.py index 741df4628..800c74544 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/treeview_template.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/treeview_template.py @@ -3,7 +3,7 @@ from typing import Any, Literal from OTAnalytics.adapter_ui.abstract_treeview_interface import AbstractTreeviewInterface -from OTAnalytics.adapter_ui.text_resources import ColumnResource +from OTAnalytics.adapter_ui.text_resources import ColumnResources from OTAnalytics.plugin_ui.customtkinter_gui.constants import tk_events from OTAnalytics.plugin_ui.customtkinter_gui.helpers import get_widget_position @@ -44,7 +44,7 @@ def get_position(self, offset: tuple[float, float] = (0.5, 0.5)) -> tuple[int, i x, y = get_widget_position(self, offset=offset) return x, y - def add_items(self, item_ids: list[ColumnResource]) -> None: + def add_items(self, item_ids: ColumnResources) -> None: for id in item_ids: cell_values = tuple(id.values[column] for column in self["columns"]) self.insert(parent="", index="end", iid=id.id, text="", values=cell_values) From 46e46dadcbee5d855910de0ccd40ba7870759f1c Mon Sep 17 00:00:00 2001 From: Lars Briem Date: Wed, 10 Apr 2024 13:46:56 +0200 Subject: [PATCH 06/12] Refactor text resource handling --- OTAnalytics/adapter_ui/text_resources.py | 21 ++++++++++++------- .../customtkinter_gui/frame_files.py | 3 ++- .../customtkinter_gui/frame_flows.py | 3 ++- .../customtkinter_gui/frame_videos.py | 3 ++- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/OTAnalytics/adapter_ui/text_resources.py b/OTAnalytics/adapter_ui/text_resources.py index 931401256..4e31051d0 100644 --- a/OTAnalytics/adapter_ui/text_resources.py +++ b/OTAnalytics/adapter_ui/text_resources.py @@ -16,22 +16,27 @@ class ColumnResource: class ColumnResources: - def __init__(self, resources: list[ColumnResource]) -> None: + def __init__( + self, resources: list[ColumnResource], lookup_column: str = COLUMN_NAME + ) -> None: self._resources = resources + self._lookup_column = lookup_column self._to_id = self._create_to_id(resources) self._to_name = self._create_to_name(resources) - @staticmethod - def _create_to_id(sections: list[ColumnResource]) -> dict[str, str]: - return {resource.values[COLUMN_NAME]: resource.id for resource in sections} + def _create_to_id(self, resources: list[ColumnResource]) -> dict[str, str]: + return { + resource.values[self._lookup_column]: resource.id for resource in resources + } - @staticmethod - def _create_to_name(sections: list[ColumnResource]) -> dict[str, str]: - return {resource.id: resource.values[COLUMN_NAME] for resource in sections} + def _create_to_name(self, resources: list[ColumnResource]) -> dict[str, str]: + return { + resource.id: resource.values[self._lookup_column] for resource in resources + } @property def names(self) -> list[str]: - return [resource.values[COLUMN_NAME] for resource in self._resources] + return [resource.values[self._lookup_column] for resource in self._resources] def get_name_for(self, resource_id: str) -> str: return self._to_name.get(resource_id, "") diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/frame_files.py b/OTAnalytics/plugin_ui/customtkinter_gui/frame_files.py index 1ef0cc0b5..fef6c46d5 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/frame_files.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/frame_files.py @@ -107,7 +107,8 @@ def update_items(self) -> None: [ self.__to_resource(file=file, video_loaded=video_loaded) for file, video_loaded in zip(track_files, track_files_have_videos) - ] + ], + lookup_column=COLUMN_FILE, ) self.add_items(item_ids=item_ids) diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/frame_flows.py b/OTAnalytics/plugin_ui/customtkinter_gui/frame_flows.py index c5c4c1f05..a4140a966 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/frame_flows.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/frame_flows.py @@ -132,7 +132,8 @@ def update_items(self) -> None: item_ids = ColumnResources( sorted( [self.__to_resource(flow) for flow in self._viewmodel.get_all_flows()] - ) + ), + lookup_column=COLUMN_FLOW, ) self.add_items(item_ids=item_ids) diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/frame_videos.py b/OTAnalytics/plugin_ui/customtkinter_gui/frame_videos.py index 0cb9eb734..09132104a 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/frame_videos.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/frame_videos.py @@ -96,7 +96,8 @@ def _on_double_click(self, event: Any) -> None: def update_items(self) -> None: self.delete(*self.get_children()) item_ids = ColumnResources( - [self.__to_resource(video) for video in self._viewmodel.get_all_videos()] + [self.__to_resource(video) for video in self._viewmodel.get_all_videos()], + lookup_column=COLUMN_VIDEO, ) self.add_items(item_ids=item_ids) From c27c94a133a5fcb7db8356067154b7eccc0f8215 Mon Sep 17 00:00:00 2001 From: Lars Briem Date: Wed, 10 Apr 2024 13:55:05 +0200 Subject: [PATCH 07/12] Add svz metadata to type system --- OTAnalytics/adapter_ui/view_model.py | 5 +- OTAnalytics/application/application.py | 3 +- OTAnalytics/application/datastore.py | 2 +- OTAnalytics/application/project.py | 51 +++++++++++-- .../application/use_cases/update_project.py | 6 +- .../customtkinter_gui/dummy_viewmodel.py | 35 ++++++++- .../customtkinter_gui/frame_project.py | 5 +- tests/OTAnalytics/application/test_project.py | 74 +++++++++++++++++-- .../use_cases/test_update_project.py | 41 ++++++++-- 9 files changed, 190 insertions(+), 32 deletions(-) diff --git a/OTAnalytics/adapter_ui/view_model.py b/OTAnalytics/adapter_ui/view_model.py index de1e8f146..50da1850b 100644 --- a/OTAnalytics/adapter_ui/view_model.py +++ b/OTAnalytics/adapter_ui/view_model.py @@ -17,7 +17,7 @@ from OTAnalytics.adapter_ui.abstract_frame_tracks import AbstractFrameTracks from OTAnalytics.adapter_ui.abstract_main_window import AbstractMainWindow from OTAnalytics.adapter_ui.abstract_treeview_interface import AbstractTreeviewInterface -from OTAnalytics.application.project import DirectionOfStationing +from OTAnalytics.adapter_ui.text_resources import ColumnResources from OTAnalytics.domain.date import DateRange from OTAnalytics.domain.flow import Flow from OTAnalytics.domain.section import Section @@ -395,5 +395,6 @@ def set_video_control_frame(self, frame: AbstractFrame) -> None: def update_svz_metadata(self, metadata: dict) -> None: raise NotImplementedError - def get_directions_of_stationing(self) -> dict[DirectionOfStationing, str]: + @abstractmethod + def get_directions_of_stationing(self) -> ColumnResources: raise NotImplementedError diff --git a/OTAnalytics/application/application.py b/OTAnalytics/application/application.py index b21761eac..7ebd871fc 100644 --- a/OTAnalytics/application/application.py +++ b/OTAnalytics/application/application.py @@ -8,6 +8,7 @@ ExportFormat, ) from OTAnalytics.application.datastore import Datastore +from OTAnalytics.application.project import SvzMetadata from OTAnalytics.application.state import ( ActionState, FileState, @@ -613,7 +614,7 @@ def update_project_name(self, name: str) -> None: def update_project_start_date(self, start_date: datetime | None) -> None: self._project_updater.update_start_date(start_date) - def update_svz_metadata(self, metadata: dict) -> None: + def update_svz_metadata(self, metadata: SvzMetadata) -> None: self._project_updater.update_svz_metadata(metadata) def get_track_repository_size(self) -> int: diff --git a/OTAnalytics/application/datastore.py b/OTAnalytics/application/datastore.py index 5dbfae87d..f6a5d0778 100644 --- a/OTAnalytics/application/datastore.py +++ b/OTAnalytics/application/datastore.py @@ -212,7 +212,7 @@ def __init__( self._video_repository = video_repository self._track_to_video_repository = track_to_video_repository self._progressbar = progressbar - self.project = Project(name="", start_date=None, metadata={}) + self.project = Project(name="", start_date=None, metadata=None) def register_video_observer(self, observer: VideoListObserver) -> None: self._video_repository.register_videos_observer(observer) diff --git a/OTAnalytics/application/project.py b/OTAnalytics/application/project.py index 89be6a4e6..e3eb66f45 100644 --- a/OTAnalytics/application/project.py +++ b/OTAnalytics/application/project.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass, field +from dataclasses import dataclass from datetime import datetime from enum import Enum from typing import Optional @@ -14,20 +14,61 @@ COORDINATE_Y: str = "coordinate_y" +class DirectionOfStationingParseError(Exception): + pass + + class DirectionOfStationing(Enum): - IN_DIRECTION = 1 - OPPOSITE_DIRECTION = 2 + IN_DIRECTION = "1" + OPPOSITE_DIRECTION = "2" + + def serialize(self) -> str: + return self.value + + @staticmethod + def parse(direction: str) -> "DirectionOfStationing": + match direction: + case DirectionOfStationing.IN_DIRECTION.value: + return DirectionOfStationing.IN_DIRECTION + case DirectionOfStationing.OPPOSITE_DIRECTION.value: + return DirectionOfStationing.OPPOSITE_DIRECTION + case _: + raise DirectionOfStationingParseError( + f"Unable to parse not existing direction '{direction}'" + ) + + +@dataclass +class SvzMetadata: + tk_number: str | None + counting_location_number: str | None + direction: DirectionOfStationing | None + remark: str | None + coordinate_x: str | None + coordinate_y: str | None + + def to_dict(self) -> dict: + return { + TK_NUMBER: self.tk_number if self.tk_number else None, + COUNTING_LOCATION_NUMBER: ( + self.counting_location_number if self.counting_location_number else None + ), + DIRECTION: self.direction.serialize() if self.direction else None, + REMARK: self.remark if self.remark else None, + COORDINATE_X: self.coordinate_x if self.coordinate_x else None, + COORDINATE_Y: self.coordinate_y if self.coordinate_y else None, + } @dataclass class Project: name: str start_date: Optional[datetime] = None - metadata: Optional[dict] = field(default_factory=lambda: {}) + metadata: Optional[SvzMetadata] = None def to_dict(self) -> dict: return { NAME: self.name, START_DATE: self.start_date.timestamp() if self.start_date else None, - METADATA: self.metadata if METADATA else None, + METADATA: self.metadata.to_dict() if self.metadata else None, } diff --git a/OTAnalytics/application/use_cases/update_project.py b/OTAnalytics/application/use_cases/update_project.py index e03e92fa6..3517f25c3 100644 --- a/OTAnalytics/application/use_cases/update_project.py +++ b/OTAnalytics/application/use_cases/update_project.py @@ -2,7 +2,7 @@ from typing import Optional from OTAnalytics.application.datastore import Datastore -from OTAnalytics.application.project import Project +from OTAnalytics.application.project import Project, SvzMetadata from OTAnalytics.domain.observer import OBSERVER, Subject @@ -14,7 +14,7 @@ def __init__(self, datastore: Datastore) -> None: self._subject = Subject[Project]() def __call__( - self, name: str, start_date: Optional[datetime], metadata: Optional[dict] + self, name: str, start_date: Optional[datetime], metadata: Optional[SvzMetadata] ) -> None: project = Project(name=name, start_date=start_date, metadata=metadata) self._datastore.project = project @@ -35,7 +35,7 @@ def update_start_date(self, start_date: Optional[datetime]) -> None: def register(self, observer: OBSERVER[Project]) -> None: self._subject.register(observer) - def update_svz_metadata(self, metadata: dict) -> None: + def update_svz_metadata(self, metadata: SvzMetadata) -> None: old_project = self._datastore.project new_project = Project(old_project.name, old_project.start_date, metadata) self._datastore.project = new_project diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py b/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py index 293b3e9b0..53c390cb7 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py @@ -58,6 +58,16 @@ from OTAnalytics.application.logger import logger from OTAnalytics.application.parser.flow_parser import FlowParser from OTAnalytics.application.playback import SkipTime +from OTAnalytics.application.project import ( + COORDINATE_X, + COORDINATE_Y, + COUNTING_LOCATION_NUMBER, + DIRECTION, + REMARK, + TK_NUMBER, + DirectionOfStationing, + SvzMetadata, +) from OTAnalytics.application.use_cases.config import MissingDate from OTAnalytics.application.use_cases.config_has_changed import NoExistingConfigFound from OTAnalytics.application.use_cases.cut_tracks_with_sections import CutTracksDto @@ -1701,7 +1711,24 @@ def set_button_quick_save_config( self._button_quick_save_config = button_quick_save_config def update_svz_metadata(self, metadata: dict) -> None: - self._application.update_svz_metadata(metadata) - - def get_directions_of_stationing(self) -> dict: - return DIRECTIONS_OF_STATIONING + svz_metadata = SvzMetadata( + tk_number=metadata[TK_NUMBER], + counting_location_number=metadata[COUNTING_LOCATION_NUMBER], + direction=( + DirectionOfStationing.parse(metadata[DIRECTION]) + if metadata[DIRECTION] + else None + ), + remark=metadata[REMARK], + coordinate_x=metadata[COORDINATE_X], + coordinate_y=metadata[COORDINATE_Y], + ) + self._application.update_svz_metadata(svz_metadata) + + def get_directions_of_stationing(self) -> ColumnResources: + return ColumnResources( + [ + ColumnResource(id=key.serialize(), values={COLUMN_NAME: value}) + for key, value in DIRECTIONS_OF_STATIONING.items() + ] + ) diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py b/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py index ab9407fab..d1526ae54 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py @@ -195,6 +195,7 @@ class FrameSvzMetadata(EmbeddedCTkFrame): def __init__(self, viewmodel: ViewModel, **kwargs: Any) -> None: super().__init__(**kwargs) self._viewmodel = viewmodel + self._directions = self._viewmodel.get_directions_of_stationing() self._tk_number = tkinter.StringVar() self._counting_location_number = tkinter.StringVar() self._coordinate_x = tkinter.StringVar() @@ -238,7 +239,7 @@ def _get_widgets(self) -> None: self._entry_direction = CTkComboBox( master=self, variable=self._direction, - values=list(self._viewmodel.get_directions_of_stationing().values()), + values=self._directions.names, ) self._label_remark = CTkLabel(master=self, text="Bemerkung") self._entry_remark = CTkEntry( @@ -311,7 +312,7 @@ def __build_metadata(self) -> dict: return { TK_NUMBER: self._tk_number.get(), COUNTING_LOCATION_NUMBER: self._counting_location_number.get(), - DIRECTION: self._direction.get(), + DIRECTION: self._directions.get_id_for(self._direction.get()), REMARK: self._remark.get(), COORDINATE_X: self._coordinate_x.get(), COORDINATE_Y: self._coordinate_y.get(), diff --git a/tests/OTAnalytics/application/test_project.py b/tests/OTAnalytics/application/test_project.py index 2489915b0..48630c950 100644 --- a/tests/OTAnalytics/application/test_project.py +++ b/tests/OTAnalytics/application/test_project.py @@ -1,17 +1,79 @@ from datetime import datetime -from OTAnalytics.application.project import METADATA, NAME, START_DATE, Project +from OTAnalytics.application.project import ( + COORDINATE_X, + COORDINATE_Y, + COUNTING_LOCATION_NUMBER, + DIRECTION, + METADATA, + NAME, + REMARK, + START_DATE, + TK_NUMBER, + DirectionOfStationing, + Project, + SvzMetadata, +) + + +class TestSvzMetadata: + def test_to_dict(self) -> None: + tk_number = "1" + counting_location_number = "2" + direction = "1" + remark = "something" + coordinate_x = "1.2" + coordinate_y = "3.4" + metadata = SvzMetadata( + tk_number=tk_number, + counting_location_number=counting_location_number, + direction=DirectionOfStationing.parse(direction), + remark=remark, + coordinate_x=coordinate_x, + coordinate_y=coordinate_y, + ) + + actual = metadata.to_dict() + + expected = { + TK_NUMBER: tk_number, + COUNTING_LOCATION_NUMBER: counting_location_number, + DIRECTION: direction, + REMARK: remark, + COORDINATE_X: coordinate_x, + COORDINATE_Y: coordinate_y, + } + + assert actual == expected class TestProject: def test_to_dict(self) -> None: name = "some" - timestamp = 12345678 - metadata = {"metadata": "svz"} - project = Project( - name=name, start_date=datetime.fromtimestamp(timestamp), metadata=metadata + timestamp = 12345678.0 + tk_number = "1" + counting_location_number = "2" + direction = "1" + remark = "something" + coordinate_x = "1.2" + coordinate_y = "3.4" + metadata = SvzMetadata( + tk_number=tk_number, + counting_location_number=counting_location_number, + direction=DirectionOfStationing.parse(direction), + remark=remark, + coordinate_x=coordinate_x, + coordinate_y=coordinate_y, ) + start_date = datetime.fromtimestamp(timestamp) + project = Project(name=name, start_date=start_date, metadata=metadata) result = project.to_dict() - assert result == {NAME: name, START_DATE: timestamp, METADATA: metadata} + expected_metadata = metadata.to_dict() + expected_result = { + NAME: name, + START_DATE: timestamp, + METADATA: expected_metadata, + } + assert result == expected_result diff --git a/tests/OTAnalytics/application/use_cases/test_update_project.py b/tests/OTAnalytics/application/use_cases/test_update_project.py index b13ad7032..ea4b5ef0b 100644 --- a/tests/OTAnalytics/application/use_cases/test_update_project.py +++ b/tests/OTAnalytics/application/use_cases/test_update_project.py @@ -4,13 +4,31 @@ import pytest from OTAnalytics.application.datastore import Datastore -from OTAnalytics.application.project import Project +from OTAnalytics.application.project import DirectionOfStationing, Project, SvzMetadata from OTAnalytics.application.use_cases.update_project import ProjectUpdater @pytest.fixture -def my_project() -> Project: - return Project("My Project", datetime(2022, 1, 1, 13), {"metadata": "default data"}) +def svz_metadata() -> SvzMetadata: + tk_number = "1" + counting_location_number = "2" + direction = "1" + remark = "something" + coordinate_x = "1.2" + coordinate_y = "3.4" + return SvzMetadata( + tk_number=tk_number, + counting_location_number=counting_location_number, + direction=DirectionOfStationing.parse(direction), + remark=remark, + coordinate_x=coordinate_x, + coordinate_y=coordinate_y, + ) + + +@pytest.fixture +def my_project(svz_metadata: SvzMetadata) -> Project: + return Project("My Project", datetime(2022, 1, 1, 13), svz_metadata) @pytest.fixture @@ -27,7 +45,7 @@ def project_metadata() -> dict: class TestUpdateProject: def test_update( - self, datastore: Mock, my_project: Project, project_metadata: dict + self, datastore: Mock, my_project: Project, svz_metadata: SvzMetadata ) -> None: new_project_name = "My New Project" new_project_start_date = datetime(2000, 2, 2, 13) @@ -35,9 +53,9 @@ def test_update( observer = Mock() project_updater.register(observer) - project_updater(new_project_name, new_project_start_date, project_metadata) + project_updater(new_project_name, new_project_start_date, svz_metadata) expected_project = Project( - new_project_name, new_project_start_date, project_metadata + new_project_name, new_project_start_date, svz_metadata ) assert datastore.project == expected_project @@ -72,9 +90,16 @@ def test_update_start_date(self, datastore: Mock, my_project: Project) -> None: observer.assert_called_once_with(expected_project) def test_update_svz_metadata( - self, datastore: Mock, my_project: Project, project_metadata: dict + self, datastore: Mock, my_project: Project, svz_metadata: SvzMetadata ) -> None: - new_metadata = {"svz": "new metadata"} + new_metadata = SvzMetadata( + tk_number=svz_metadata.tk_number, + counting_location_number=svz_metadata.counting_location_number, + direction=svz_metadata.direction, + remark="new metadata", + coordinate_x=svz_metadata.coordinate_x, + coordinate_y=svz_metadata.coordinate_y, + ) project_updater = ProjectUpdater(datastore) observer = Mock() project_updater.register(observer) From ae1ebdf3cbdc3176b7bffe05bbdf316d7a5292cf Mon Sep 17 00:00:00 2001 From: Lars Briem Date: Wed, 10 Apr 2024 14:13:59 +0200 Subject: [PATCH 08/12] Add weather to metadata --- OTAnalytics/adapter_ui/ui_texts.py | 10 +++- OTAnalytics/adapter_ui/view_model.py | 3 + OTAnalytics/application/project.py | 33 +++++++++++ .../customtkinter_gui/dummy_viewmodel.py | 15 ++++- .../customtkinter_gui/frame_project.py | 59 ++++++++++++------- tests/OTAnalytics/application/test_project.py | 7 +++ .../use_cases/test_update_project.py | 15 +++-- 7 files changed, 113 insertions(+), 29 deletions(-) diff --git a/OTAnalytics/adapter_ui/ui_texts.py b/OTAnalytics/adapter_ui/ui_texts.py index d4ca90762..b3ebd635f 100644 --- a/OTAnalytics/adapter_ui/ui_texts.py +++ b/OTAnalytics/adapter_ui/ui_texts.py @@ -1,6 +1,14 @@ -from OTAnalytics.application.project import DirectionOfStationing +from OTAnalytics.application.project import DirectionOfStationing, WeatherType DIRECTIONS_OF_STATIONING = { DirectionOfStationing.IN_DIRECTION: "In Stationierungsrichtung", DirectionOfStationing.OPPOSITE_DIRECTION: "Gegen Stationierungsrichtung", } + +WEATHER_TYPES = { + WeatherType.SUN: "sonnig", + WeatherType.CLOUD: "bewölkt", + WeatherType.RAIN: "Regen", + WeatherType.SNOW: "Schnee", + WeatherType.FOG: "Nebel", +} diff --git a/OTAnalytics/adapter_ui/view_model.py b/OTAnalytics/adapter_ui/view_model.py index 50da1850b..84f979380 100644 --- a/OTAnalytics/adapter_ui/view_model.py +++ b/OTAnalytics/adapter_ui/view_model.py @@ -398,3 +398,6 @@ def update_svz_metadata(self, metadata: dict) -> None: @abstractmethod def get_directions_of_stationing(self) -> ColumnResources: raise NotImplementedError + + def get_weather_types(self) -> ColumnResources: + raise NotImplementedError diff --git a/OTAnalytics/application/project.py b/OTAnalytics/application/project.py index e3eb66f45..b7b56044b 100644 --- a/OTAnalytics/application/project.py +++ b/OTAnalytics/application/project.py @@ -9,6 +9,7 @@ TK_NUMBER: str = "tk_number" COUNTING_LOCATION_NUMBER: str = "counting_location_number" DIRECTION: str = "direction" +WEATHER: str = "weather" REMARK: str = "remark" COORDINATE_X: str = "coordinate_x" COORDINATE_Y: str = "coordinate_y" @@ -38,11 +39,42 @@ def parse(direction: str) -> "DirectionOfStationing": ) +class WeatherTypeParseError(Exception): + pass + + +class WeatherType(Enum): + SUN = "1" + CLOUD = "2" + RAIN = "3" + SNOW = "4" + FOG = "5" + + def serialize(self) -> str: + return self.value + + @staticmethod + def parse(weather_type: str) -> "WeatherType": + for type in [ + WeatherType.SUN, + WeatherType.CLOUD, + WeatherType.RAIN, + WeatherType.SNOW, + WeatherType.FOG, + ]: + if type.value == weather_type: + return type + raise WeatherTypeParseError( + f"Unable to parse not existing weather type '{weather_type}'" + ) + + @dataclass class SvzMetadata: tk_number: str | None counting_location_number: str | None direction: DirectionOfStationing | None + weather: WeatherType | None remark: str | None coordinate_x: str | None coordinate_y: str | None @@ -54,6 +86,7 @@ def to_dict(self) -> dict: self.counting_location_number if self.counting_location_number else None ), DIRECTION: self.direction.serialize() if self.direction else None, + WEATHER: self.weather.serialize() if self.weather else None, REMARK: self.remark if self.remark else None, COORDINATE_X: self.coordinate_x if self.coordinate_x else None, COORDINATE_Y: self.coordinate_y if self.coordinate_y else None, diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py b/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py index 53c390cb7..e078b4c5a 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py @@ -35,7 +35,7 @@ ColumnResource, ColumnResources, ) -from OTAnalytics.adapter_ui.ui_texts import DIRECTIONS_OF_STATIONING +from OTAnalytics.adapter_ui.ui_texts import DIRECTIONS_OF_STATIONING, WEATHER_TYPES from OTAnalytics.adapter_ui.view_model import ( MetadataProvider, MissingCoordinate, @@ -65,8 +65,10 @@ DIRECTION, REMARK, TK_NUMBER, + WEATHER, DirectionOfStationing, SvzMetadata, + WeatherType, ) from OTAnalytics.application.use_cases.config import MissingDate from OTAnalytics.application.use_cases.config_has_changed import NoExistingConfigFound @@ -1719,6 +1721,9 @@ def update_svz_metadata(self, metadata: dict) -> None: if metadata[DIRECTION] else None ), + weather=( + WeatherType.parse(metadata[WEATHER]) if metadata[WEATHER] else None + ), remark=metadata[REMARK], coordinate_x=metadata[COORDINATE_X], coordinate_y=metadata[COORDINATE_Y], @@ -1732,3 +1737,11 @@ def get_directions_of_stationing(self) -> ColumnResources: for key, value in DIRECTIONS_OF_STATIONING.items() ] ) + + def get_weather_types(self) -> ColumnResources: + return ColumnResources( + [ + ColumnResource(id=key.serialize(), values={COLUMN_NAME: value}) + for key, value in WEATHER_TYPES.items() + ] + ) diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py b/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py index d1526ae54..8f2de27d9 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py @@ -14,6 +14,7 @@ DIRECTION, REMARK, TK_NUMBER, + WEATHER, ) from OTAnalytics.plugin_ui.customtkinter_gui.button_quick_save_config import ( ButtonQuickSaveConfig, @@ -196,12 +197,14 @@ def __init__(self, viewmodel: ViewModel, **kwargs: Any) -> None: super().__init__(**kwargs) self._viewmodel = viewmodel self._directions = self._viewmodel.get_directions_of_stationing() + self._weather_types = self._viewmodel.get_weather_types() self._tk_number = tkinter.StringVar() self._counting_location_number = tkinter.StringVar() - self._coordinate_x = tkinter.StringVar() - self._coordinate_y = tkinter.StringVar() self._direction = tkinter.StringVar() + self._weather = tkinter.StringVar() self._remark = tkinter.StringVar() + self._coordinate_x = tkinter.StringVar() + self._coordinate_y = tkinter.StringVar() self._get_widgets() self._place_widgets() self.introduce_to_viewmodel() @@ -222,6 +225,24 @@ def _get_widgets(self) -> None: textvariable=self._counting_location_number, placeholder_text="Zählstellennummer", ) + self._label_direction = CTkLabel(master=self, text="Ausrichtung") + self._entry_direction = CTkComboBox( + master=self, + variable=self._direction, + values=self._directions.names, + ) + self._label_weather = CTkLabel(master=self, text="Wetter") + self._entry_weather = CTkComboBox( + master=self, + variable=self._weather, + values=self._weather_types.names, + ) + self._label_remark = CTkLabel(master=self, text="Bemerkung") + self._entry_remark = CTkEntry( + master=self, + textvariable=self._remark, + placeholder_text="Bemerkung", + ) self._label_coordinate = CTkLabel(master=self, text="Geokoordinate") self._label_coordinate_x = CTkLabel(master=self, text="X") self._entry_coordinate_x = CTkEntry( @@ -235,18 +256,6 @@ def _get_widgets(self) -> None: textvariable=self._coordinate_y, placeholder_text="Y Koordinate", ) - self._label_direction = CTkLabel(master=self, text="Ausrichtung") - self._entry_direction = CTkComboBox( - master=self, - variable=self._direction, - values=self._directions.names, - ) - self._label_remark = CTkLabel(master=self, text="Bemerkung") - self._entry_remark = CTkEntry( - master=self, - textvariable=self._remark, - placeholder_text="Bemerkung", - ) def introduce_to_viewmodel(self) -> None: pass @@ -273,26 +282,32 @@ def _place_widgets(self) -> None: self._entry_direction.grid( row=2, column=1, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY ) - self._label_remark.grid( + self._label_weather.grid( row=3, column=0, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY_WEST ) - self._entry_remark.grid( + self._entry_weather.grid( row=3, column=1, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY ) + self._label_remark.grid( + row=4, column=0, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY_WEST + ) + self._entry_remark.grid( + row=4, column=1, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY + ) self._label_coordinate.grid( - row=4, column=0, columnspan=2, padx=PADX, pady=PADY, sticky=STICKY + row=5, column=0, columnspan=2, padx=PADX, pady=PADY, sticky=STICKY ) self._label_coordinate_x.grid( - row=5, column=0, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY + row=6, column=0, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY ) self._entry_coordinate_x.grid( - row=5, column=1, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY + row=6, column=1, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY ) self._label_coordinate_y.grid( - row=6, column=0, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY + row=7, column=0, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY ) self._entry_coordinate_y.grid( - row=6, column=1, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY + row=7, column=1, columnspan=1, padx=PADX, pady=PADY, sticky=STICKY ) def _wire_callbacks(self) -> None: @@ -301,6 +316,7 @@ def _wire_callbacks(self) -> None: "write", callback=self._update_metadata ) self._direction.trace_add("write", callback=self._update_metadata) + self._weather.trace_add("write", callback=self._update_metadata) self._remark.trace_add("write", callback=self._update_metadata) self._coordinate_x.trace_add("write", callback=self._update_metadata) self._coordinate_y.trace_add("write", callback=self._update_metadata) @@ -313,6 +329,7 @@ def __build_metadata(self) -> dict: TK_NUMBER: self._tk_number.get(), COUNTING_LOCATION_NUMBER: self._counting_location_number.get(), DIRECTION: self._directions.get_id_for(self._direction.get()), + WEATHER: self._weather_types.get_id_for(self._weather.get()), REMARK: self._remark.get(), COORDINATE_X: self._coordinate_x.get(), COORDINATE_Y: self._coordinate_y.get(), diff --git a/tests/OTAnalytics/application/test_project.py b/tests/OTAnalytics/application/test_project.py index 48630c950..fa5d73454 100644 --- a/tests/OTAnalytics/application/test_project.py +++ b/tests/OTAnalytics/application/test_project.py @@ -10,9 +10,11 @@ REMARK, START_DATE, TK_NUMBER, + WEATHER, DirectionOfStationing, Project, SvzMetadata, + WeatherType, ) @@ -21,6 +23,7 @@ def test_to_dict(self) -> None: tk_number = "1" counting_location_number = "2" direction = "1" + weather = "2" remark = "something" coordinate_x = "1.2" coordinate_y = "3.4" @@ -28,6 +31,7 @@ def test_to_dict(self) -> None: tk_number=tk_number, counting_location_number=counting_location_number, direction=DirectionOfStationing.parse(direction), + weather=WeatherType.parse(weather), remark=remark, coordinate_x=coordinate_x, coordinate_y=coordinate_y, @@ -39,6 +43,7 @@ def test_to_dict(self) -> None: TK_NUMBER: tk_number, COUNTING_LOCATION_NUMBER: counting_location_number, DIRECTION: direction, + WEATHER: weather, REMARK: remark, COORDINATE_X: coordinate_x, COORDINATE_Y: coordinate_y, @@ -54,6 +59,7 @@ def test_to_dict(self) -> None: tk_number = "1" counting_location_number = "2" direction = "1" + weather = "2" remark = "something" coordinate_x = "1.2" coordinate_y = "3.4" @@ -61,6 +67,7 @@ def test_to_dict(self) -> None: tk_number=tk_number, counting_location_number=counting_location_number, direction=DirectionOfStationing.parse(direction), + weather=WeatherType.parse(weather), remark=remark, coordinate_x=coordinate_x, coordinate_y=coordinate_y, diff --git a/tests/OTAnalytics/application/use_cases/test_update_project.py b/tests/OTAnalytics/application/use_cases/test_update_project.py index ea4b5ef0b..c316461a6 100644 --- a/tests/OTAnalytics/application/use_cases/test_update_project.py +++ b/tests/OTAnalytics/application/use_cases/test_update_project.py @@ -4,7 +4,12 @@ import pytest from OTAnalytics.application.datastore import Datastore -from OTAnalytics.application.project import DirectionOfStationing, Project, SvzMetadata +from OTAnalytics.application.project import ( + DirectionOfStationing, + Project, + SvzMetadata, + WeatherType, +) from OTAnalytics.application.use_cases.update_project import ProjectUpdater @@ -13,6 +18,7 @@ def svz_metadata() -> SvzMetadata: tk_number = "1" counting_location_number = "2" direction = "1" + weather = "2" remark = "something" coordinate_x = "1.2" coordinate_y = "3.4" @@ -20,6 +26,7 @@ def svz_metadata() -> SvzMetadata: tk_number=tk_number, counting_location_number=counting_location_number, direction=DirectionOfStationing.parse(direction), + weather=WeatherType.parse(weather), remark=remark, coordinate_x=coordinate_x, coordinate_y=coordinate_y, @@ -38,11 +45,6 @@ def datastore(my_project: Project) -> Mock: return datastore -@pytest.fixture -def project_metadata() -> dict: - return {"new": "project metadata"} - - class TestUpdateProject: def test_update( self, datastore: Mock, my_project: Project, svz_metadata: SvzMetadata @@ -96,6 +98,7 @@ def test_update_svz_metadata( tk_number=svz_metadata.tk_number, counting_location_number=svz_metadata.counting_location_number, direction=svz_metadata.direction, + weather=svz_metadata.weather, remark="new metadata", coordinate_x=svz_metadata.coordinate_x, coordinate_y=svz_metadata.coordinate_y, From 8c26204ce1c5b28104e54586a7d5108931626c3c Mon Sep 17 00:00:00 2001 From: Lars Briem Date: Wed, 10 Apr 2024 14:33:43 +0200 Subject: [PATCH 09/12] Show loaded svz metadata --- .../adapter_ui/abstract_frame_project.py | 10 ++++++ OTAnalytics/adapter_ui/view_model.py | 10 +++++- OTAnalytics/plugin_parser/otconfig_parser.py | 35 +++++++++++++++++-- .../customtkinter_gui/dummy_viewmodel.py | 17 ++++++++- .../customtkinter_gui/frame_project.py | 18 ++++++++-- 5 files changed, 83 insertions(+), 7 deletions(-) diff --git a/OTAnalytics/adapter_ui/abstract_frame_project.py b/OTAnalytics/adapter_ui/abstract_frame_project.py index b7ded7fbe..fdd0085f3 100644 --- a/OTAnalytics/adapter_ui/abstract_frame_project.py +++ b/OTAnalytics/adapter_ui/abstract_frame_project.py @@ -15,3 +15,13 @@ def update(self, name: str, start_date: Optional[datetime]) -> None: @abstractmethod def set_enabled_general_buttons(self, enabled: bool) -> None: raise NotImplementedError + + +class AbstractFrameSvzMetadata: + @abstractmethod + def introduce_to_viewmodel(self) -> None: + pass + + @abstractmethod + def update(self, metadata: dict) -> None: + pass diff --git a/OTAnalytics/adapter_ui/view_model.py b/OTAnalytics/adapter_ui/view_model.py index 84f979380..539c4be61 100644 --- a/OTAnalytics/adapter_ui/view_model.py +++ b/OTAnalytics/adapter_ui/view_model.py @@ -10,7 +10,10 @@ from OTAnalytics.adapter_ui.abstract_frame import AbstractFrame from OTAnalytics.adapter_ui.abstract_frame_canvas import AbstractFrameCanvas from OTAnalytics.adapter_ui.abstract_frame_filter import AbstractFrameFilter -from OTAnalytics.adapter_ui.abstract_frame_project import AbstractFrameProject +from OTAnalytics.adapter_ui.abstract_frame_project import ( + AbstractFrameProject, + AbstractFrameSvzMetadata, +) from OTAnalytics.adapter_ui.abstract_frame_track_plotting import ( AbstractFrameTrackPlotting, ) @@ -399,5 +402,10 @@ def update_svz_metadata(self, metadata: dict) -> None: def get_directions_of_stationing(self) -> ColumnResources: raise NotImplementedError + @abstractmethod def get_weather_types(self) -> ColumnResources: raise NotImplementedError + + @abstractmethod + def set_svz_metadata_frame(self, frame: AbstractFrameSvzMetadata) -> None: + raise NotImplementedError diff --git a/OTAnalytics/plugin_parser/otconfig_parser.py b/OTAnalytics/plugin_parser/otconfig_parser.py index f5a587504..0170b4621 100644 --- a/OTAnalytics/plugin_parser/otconfig_parser.py +++ b/OTAnalytics/plugin_parser/otconfig_parser.py @@ -14,7 +14,19 @@ StartDateMissing, ) from OTAnalytics.application.parser.flow_parser import FlowParser -from OTAnalytics.application.project import Project +from OTAnalytics.application.project import ( + COORDINATE_X, + COORDINATE_Y, + COUNTING_LOCATION_NUMBER, + DIRECTION, + REMARK, + TK_NUMBER, + WEATHER, + DirectionOfStationing, + Project, + SvzMetadata, + WeatherType, +) from OTAnalytics.domain import flow, section, video from OTAnalytics.domain.flow import Flow from OTAnalytics.domain.section import Section @@ -119,7 +131,26 @@ def _parse_project(self, data: dict) -> Project: _validate_data(data, [project.NAME, project.START_DATE]) name = data[project.NAME] start_date = datetime.fromtimestamp(data[project.START_DATE], timezone.utc) - return Project(name=name, start_date=start_date) + svz_metadata = self._parse_svz_metadata(data[project.METADATA]) + return Project(name=name, start_date=start_date, metadata=svz_metadata) + + def _parse_svz_metadata(self, data: dict) -> SvzMetadata: + tk_number = data[TK_NUMBER] + counting_location_number = data[COUNTING_LOCATION_NUMBER] + direction = DirectionOfStationing.parse(data[DIRECTION]) + weather = WeatherType.parse(data[WEATHER]) + remark = data[REMARK] + coordinate_x = data[COORDINATE_X] + coordinate_y = data[COORDINATE_Y] + return SvzMetadata( + tk_number=tk_number, + counting_location_number=counting_location_number, + direction=direction, + weather=weather, + remark=remark, + coordinate_x=coordinate_x, + coordinate_y=coordinate_y, + ) def _parse_analysis(self, data: dict, base_folder: Path) -> AnalysisConfig: _validate_data( diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py b/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py index e078b4c5a..95ecb9cfc 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py @@ -13,7 +13,10 @@ from OTAnalytics.adapter_ui.abstract_frame import AbstractFrame from OTAnalytics.adapter_ui.abstract_frame_canvas import AbstractFrameCanvas from OTAnalytics.adapter_ui.abstract_frame_filter import AbstractFrameFilter -from OTAnalytics.adapter_ui.abstract_frame_project import AbstractFrameProject +from OTAnalytics.adapter_ui.abstract_frame_project import ( + AbstractFrameProject, + AbstractFrameSvzMetadata, +) from OTAnalytics.adapter_ui.abstract_frame_track_plotting import ( AbstractFrameTrackPlotting, ) @@ -582,6 +585,7 @@ def _load_otconfig(self, otconfig_file: Path) -> None: logger().info(f"{OTCONFIG} file to load: {otconfig_file}") self._application.load_otconfig(file=Path(otconfig_file)) self._show_current_project() + self._show_current_svz_metadata() def set_tracks_frame(self, tracks_frame: AbstractFrameTracks) -> None: self._frame_tracks = tracks_frame @@ -1745,3 +1749,14 @@ def get_weather_types(self) -> ColumnResources: for key, value in WEATHER_TYPES.items() ] ) + + def set_svz_metadata_frame(self, frame: AbstractFrameSvzMetadata) -> None: + self._frame_svz_metadata = frame + self._show_current_svz_metadata() + + def _show_current_svz_metadata(self) -> None: + if self._frame_svz_metadata is None: + raise MissingInjectedInstanceError(type(self._frame_svz_metadata).__name__) + project = self._application._datastore.project + if metadata := project.metadata: + self._frame_svz_metadata.update(metadata=metadata.to_dict()) diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py b/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py index 8f2de27d9..0bbd91adb 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py @@ -5,7 +5,10 @@ from customtkinter import CTkButton, CTkComboBox, CTkEntry, CTkLabel, ThemeManager -from OTAnalytics.adapter_ui.abstract_frame_project import AbstractFrameProject +from OTAnalytics.adapter_ui.abstract_frame_project import ( + AbstractFrameProject, + AbstractFrameSvzMetadata, +) from OTAnalytics.adapter_ui.view_model import ViewModel from OTAnalytics.application.project import ( COORDINATE_X, @@ -191,7 +194,7 @@ def _place_widgets(self) -> None: self.set(self._title) -class FrameSvzMetadata(EmbeddedCTkFrame): +class FrameSvzMetadata(AbstractFrameSvzMetadata, EmbeddedCTkFrame): def __init__(self, viewmodel: ViewModel, **kwargs: Any) -> None: super().__init__(**kwargs) @@ -258,7 +261,7 @@ def _get_widgets(self) -> None: ) def introduce_to_viewmodel(self) -> None: - pass + self._viewmodel.set_svz_metadata_frame(self) def _place_widgets(self) -> None: self.grid_rowconfigure((0, 1, 2, 3, 4), weight=1) @@ -335,6 +338,15 @@ def __build_metadata(self) -> dict: COORDINATE_Y: self._coordinate_y.get(), } + def update(self, metadata: dict) -> None: + self._tk_number.set(metadata[TK_NUMBER]) + self._counting_location_number.set(metadata[COUNTING_LOCATION_NUMBER]) + self._direction.set(self._directions.get_name_for(metadata[DIRECTION])) + self._weather.set(self._weather_types.get_name_for(metadata[WEATHER])) + self._remark.set(metadata[REMARK]) + self._coordinate_x.set(metadata[COORDINATE_X]) + self._coordinate_y.set(metadata[COORDINATE_Y]) + def get_default_toplevel_fg_color() -> str: return ThemeManager.theme["CTkToplevel"]["fg_color"] From 376aed2944072b9b29db58a6a6a5ea737bc19a2d Mon Sep 17 00:00:00 2001 From: Randy Seng <19281702+randy-seng@users.noreply.github.com> Date: Tue, 16 Apr 2024 12:57:07 +0200 Subject: [PATCH 10/12] Parse SVZ Metadata only when it exists in OTConfig --- OTAnalytics/plugin_parser/otconfig_parser.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/OTAnalytics/plugin_parser/otconfig_parser.py b/OTAnalytics/plugin_parser/otconfig_parser.py index 0170b4621..9aecb9c0c 100644 --- a/OTAnalytics/plugin_parser/otconfig_parser.py +++ b/OTAnalytics/plugin_parser/otconfig_parser.py @@ -131,7 +131,9 @@ def _parse_project(self, data: dict) -> Project: _validate_data(data, [project.NAME, project.START_DATE]) name = data[project.NAME] start_date = datetime.fromtimestamp(data[project.START_DATE], timezone.utc) - svz_metadata = self._parse_svz_metadata(data[project.METADATA]) + svz_metadata = None + if svz_data := data.get(project.METADATA): + svz_metadata = self._parse_svz_metadata(svz_data) return Project(name=name, start_date=start_date, metadata=svz_metadata) def _parse_svz_metadata(self, data: dict) -> SvzMetadata: From 7e6960399a7c5efc5f3d45445fbb097798a47bb0 Mon Sep 17 00:00:00 2001 From: Randy Seng <19281702+randy-seng@users.noreply.github.com> Date: Tue, 16 Apr 2024 13:49:15 +0200 Subject: [PATCH 11/12] WIP: Reset SVZ metadata on restart project --- .../customtkinter_gui/dummy_viewmodel.py | 16 +++++++++++++ .../customtkinter_gui/frame_project.py | 23 +++++++++++++------ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py b/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py index 948c5322b..01da1b14a 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py @@ -232,6 +232,7 @@ def __init__( self._frame_analysis: Optional[AbstractFrame] = None self._canvas: Optional[AbstractCanvas] = None self._frame_track_plotting: Optional[AbstractFrameTrackPlotting] = None + self._frame_svz_metadata: Optional[AbstractFrameSvzMetadata] = None self._treeview_sections: Optional[AbstractTreeviewInterface] self._treeview_flows: Optional[AbstractTreeviewInterface] self._button_quick_save_config: AbstractButtonQuickSaveConfig | None = None @@ -1653,6 +1654,7 @@ def start_new_project(self) -> None: return self._application.start_new_project() self._show_current_project() + self._show_current_svz_metadata() logger().info("Start new project.") def update_project_name(self, name: str) -> None: @@ -1665,6 +1667,7 @@ def on_start_new_project(self, _: None) -> None: self._reset_filters() self._reset_plotting_layer() self._display_preview_image() + self._show_current_svz_metadata() def _reset_filters(self) -> None: if self._frame_filter is None: @@ -1803,3 +1806,16 @@ def _show_current_svz_metadata(self) -> None: project = self._application._datastore.project if metadata := project.metadata: self._frame_svz_metadata.update(metadata=metadata.to_dict()) + else: + self._frame_svz_metadata.update({}) + + # def update_svz_metadata_in_frame(self, project: Project) -> None: + # if self._frame_svz_metadata is None: + # noqa raise MissingInjectedInstanceError(type(self._frame_svz_metadata).__name__) + # + # if project.metadata: + # metadata = project.metadata.to_dict() + # else: + # metadata = dict() + # + # self._frame_svz_metadata.update(metadata) diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py b/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py index 0bbd91adb..70280e818 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/frame_project.py @@ -339,13 +339,22 @@ def __build_metadata(self) -> dict: } def update(self, metadata: dict) -> None: - self._tk_number.set(metadata[TK_NUMBER]) - self._counting_location_number.set(metadata[COUNTING_LOCATION_NUMBER]) - self._direction.set(self._directions.get_name_for(metadata[DIRECTION])) - self._weather.set(self._weather_types.get_name_for(metadata[WEATHER])) - self._remark.set(metadata[REMARK]) - self._coordinate_x.set(metadata[COORDINATE_X]) - self._coordinate_y.set(metadata[COORDINATE_Y]) + if metadata: + self._tk_number.set(metadata[TK_NUMBER]) + self._counting_location_number.set(metadata[COUNTING_LOCATION_NUMBER]) + self._direction.set(self._directions.get_name_for(metadata[DIRECTION])) + self._weather.set(self._weather_types.get_name_for(metadata[WEATHER])) + self._remark.set(metadata[REMARK]) + self._coordinate_x.set(metadata[COORDINATE_X]) + self._coordinate_y.set(metadata[COORDINATE_Y]) + else: + self._tk_number.set("") + self._counting_location_number.set("") + self._direction.set("") + self._weather.set("") + self._remark.set("") + self._coordinate_x.set("") + self._coordinate_y.set("") def get_default_toplevel_fg_color() -> str: From 5fa6e745c6b380f784bf44b6ef21fb6787c93247 Mon Sep 17 00:00:00 2001 From: Lars Briem Date: Tue, 16 Apr 2024 14:05:05 +0200 Subject: [PATCH 12/12] Remove unused code --- .../plugin_ui/customtkinter_gui/dummy_viewmodel.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py b/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py index 01da1b14a..3462569bf 100644 --- a/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py +++ b/OTAnalytics/plugin_ui/customtkinter_gui/dummy_viewmodel.py @@ -1667,7 +1667,6 @@ def on_start_new_project(self, _: None) -> None: self._reset_filters() self._reset_plotting_layer() self._display_preview_image() - self._show_current_svz_metadata() def _reset_filters(self) -> None: if self._frame_filter is None: @@ -1808,14 +1807,3 @@ def _show_current_svz_metadata(self) -> None: self._frame_svz_metadata.update(metadata=metadata.to_dict()) else: self._frame_svz_metadata.update({}) - - # def update_svz_metadata_in_frame(self, project: Project) -> None: - # if self._frame_svz_metadata is None: - # noqa raise MissingInjectedInstanceError(type(self._frame_svz_metadata).__name__) - # - # if project.metadata: - # metadata = project.metadata.to_dict() - # else: - # metadata = dict() - # - # self._frame_svz_metadata.update(metadata)