From a2b6c7c6847f67917f2da62eea251cf763196a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20R=C3=A9aux?= Date: Fri, 13 Feb 2026 15:44:28 +0100 Subject: [PATCH] exceptions adding in closed elements --- src/warchron/controller/app_controller.py | 92 +++++++++++++++++-- .../controller/campaign_controller.py | 47 +++++----- src/warchron/controller/player_controller.py | 18 ++-- src/warchron/controller/round_controller.py | 14 ++- src/warchron/controller/war_controller.py | 50 +++++----- src/warchron/model/closure_workflow.py | 19 ++++ src/warchron/model/model.py | 2 + src/warchron/model/score_service.py | 6 ++ src/warchron/model/war.py | 2 + src/warchron/view/view.py | 11 +-- 10 files changed, 179 insertions(+), 82 deletions(-) create mode 100644 src/warchron/model/closure_workflow.py diff --git a/src/warchron/controller/app_controller.py b/src/warchron/controller/app_controller.py index 57c1958..7ac6d7c 100644 --- a/src/warchron/controller/app_controller.py +++ b/src/warchron/controller/app_controller.py @@ -31,8 +31,7 @@ class AppController: self.navigation.refresh_wars_view() self.update_window_title() self.view.on_tree_selection_changed = self.navigation.on_tree_selection_changed - self.view.on_add_campaign = self.campaigns.add_campaign - self.view.on_add_round = self.rounds.add_round + self.view.on_add_item = self.add_item def __connect(self) -> None: self.view.actionExit.triggered.connect(self.view.close) @@ -41,20 +40,25 @@ class AppController: self.view.actionSave.triggered.connect(self.save) self.view.actionSave_as.triggered.connect(self.save_as) self.view.actionAbout.triggered.connect(self.show_about) - self.view.addPlayerBtn.clicked.connect(self.players.add_player) - self.view.addWarBtn.clicked.connect(self.wars.add_war) + self.view.addPlayerBtn.clicked.connect(lambda: self.add_item(ItemType.PLAYER)) + self.view.addWarBtn.clicked.connect(lambda: self.add_item(ItemType.WAR)) self.view.majorValue.valueChanged.connect(self.wars.set_major_value) self.view.minorValue.valueChanged.connect(self.wars.set_minor_value) self.view.influenceToken.toggled.connect(self.wars.set_influence_token) - self.view.addObjectiveBtn.clicked.connect(self.wars.add_objective) - self.view.addWarParticipantBtn.clicked.connect(self.wars.add_war_participant) + self.view.addObjectiveBtn.clicked.connect( + lambda: self.add_item(ItemType.OBJECTIVE) + ) + self.view.addWarParticipantBtn.clicked.connect( + lambda: self.add_item(ItemType.WAR_PARTICIPANT) + ) self.view.endWarBtn.clicked.connect(self.wars.close_war) - self.view.addSectorBtn.clicked.connect(self.campaigns.add_sector) + self.view.addSectorBtn.clicked.connect(lambda: self.add_item(ItemType.SECTOR)) self.view.addCampaignParticipantBtn.clicked.connect( - self.campaigns.add_campaign_participant + lambda: self.add_item(ItemType.CAMPAIGN_PARTICIPANT) ) self.view.endCampaignBtn.clicked.connect(self.campaigns.close_campaign) self.view.endRoundBtn.clicked.connect(self.rounds.close_round) + self.view.on_add_item = self.add_item self.view.on_edit_item = self.edit_item self.view.on_delete_item = self.delete_item @@ -161,6 +165,72 @@ class AppController: # Command methods + def add_item(self, item_type: str) -> None: + try: + if item_type == ItemType.PLAYER: + play = self.players.create_player() + if not play: + return + self.navigation.refresh(RefreshScope.PLAYERS_LIST) + elif item_type == ItemType.WAR: + war = self.wars.create_war() + if not war: + return + self.navigation.refresh_and_select( + RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war.id + ) + elif item_type == ItemType.CAMPAIGN: + camp = self.campaigns.create_campaign() + if not camp: + return + self.navigation.refresh_and_select( + RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp.id + ) + elif item_type == ItemType.OBJECTIVE: + obj = self.wars.create_objective() + if not obj: + return + self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) + elif item_type == ItemType.WAR_PARTICIPANT: + war_part = self.wars.create_war_participant() + if not war_part: + return + self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) + elif item_type == ItemType.SECTOR: + sect = self.campaigns.create_sector() + if not sect: + return + self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) + elif item_type == ItemType.CAMPAIGN_PARTICIPANT: + camp_part = self.campaigns.create_campaign_participant() + if not camp_part: + return + self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) + elif item_type == ItemType.ROUND: + rnd = self.rounds.create_round() + if not rnd: + return + self.navigation.refresh_and_select( + RefreshScope.WARS_TREE, item_type=ItemType.ROUND, item_id=rnd.id + ) + self.is_dirty = True + except DomainError as e: + QMessageBox.warning( + self.view, + "Deletion forbidden", + str(e), + ) + except RequiresConfirmation as e: + reply = QMessageBox.question( + self.view, + "Confirm update", + str(e), + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + ) + if reply == QMessageBox.StandardButton.Yes: + e.action() + self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) + def edit_item(self, item_type: str, item_id: str) -> None: try: if item_type == ItemType.PLAYER: @@ -195,6 +265,12 @@ class AppController: self.rounds.edit_round_battle(item_id) self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) self.is_dirty = True + except DomainError as e: + QMessageBox.warning( + self.view, + "Deletion forbidden", + str(e), + ) except RequiresConfirmation as e: reply = QMessageBox.question( self.view, diff --git a/src/warchron/controller/campaign_controller.py b/src/warchron/controller/campaign_controller.py index f5ff268..022fd54 100644 --- a/src/warchron/controller/campaign_controller.py +++ b/src/warchron/controller/campaign_controller.py @@ -2,7 +2,7 @@ from typing import List, TYPE_CHECKING from PyQt6.QtWidgets import QMessageBox, QDialog -from warchron.constants import ItemType, RefreshScope +from warchron.constants import RefreshScope if TYPE_CHECKING: from warchron.controller.app_controller import AppController @@ -13,6 +13,9 @@ from warchron.controller.dtos import ( SectorDTO, RoundDTO, ) +from warchron.model.campaign import Campaign +from warchron.model.campaign_participant import CampaignParticipant +from warchron.model.sector import Sector from warchron.model.closure_service import ClosureService from warchron.view.campaign_dialog import CampaignDialog from warchron.view.campaign_participant_dialog import CampaignParticipantDialog @@ -68,9 +71,9 @@ class CampaignController: return False return True - def add_campaign(self) -> None: + def create_campaign(self) -> Campaign | None: if not self.app.navigation.selected_war_id: - return + return None dialog = CampaignDialog( self.app.view, default_month=self.app.model.get_default_campaign_values( @@ -78,18 +81,18 @@ class CampaignController: )["month"], ) if dialog.exec() != QDialog.DialogCode.Accepted: - return + return None name = dialog.get_campaign_name() month = dialog.get_campaign_month() if not self._validate_campaign_inputs(name, month): - return - camp = self.app.model.add_campaign( + return None + return self.app.model.add_campaign( self.app.navigation.selected_war_id, name, month ) - self.app.is_dirty = True - self.app.navigation.refresh_and_select( - RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp.id - ) + # self.app.is_dirty = True + # self.app.navigation.refresh_and_select( + # RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp.id + # ) def edit_campaign(self, campaign_id: str) -> None: camp = self.app.model.get_campaign(campaign_id) @@ -128,9 +131,9 @@ class CampaignController: # Campaign participant methods - def add_campaign_participant(self) -> None: + def create_campaign_participant(self) -> CampaignParticipant | None: if not self.app.navigation.selected_campaign_id: - return + return None participants = self.app.model.get_available_war_participants( self.app.navigation.selected_campaign_id ) @@ -140,17 +143,15 @@ class CampaignController: ] dialog = CampaignParticipantDialog(self.app.view, participants=part_opts) if dialog.exec() != QDialog.DialogCode.Accepted: - return + return None player_id = dialog.get_player_id() leader = dialog.get_participant_leader() theme = dialog.get_participant_theme() if not player_id: - return - self.app.model.add_campaign_participant( + return None + return self.app.model.add_campaign_participant( self.app.navigation.selected_campaign_id, player_id, leader, theme ) - self.app.is_dirty = True - self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) def edit_campaign_participant(self, participant_id: str) -> None: camp_part = self.app.model.get_campaign_participant(participant_id) @@ -191,9 +192,9 @@ class CampaignController: # allow same objectives in different fields? return True - def add_sector(self) -> None: + def create_sector(self) -> Sector | None: if not self.app.navigation.selected_campaign_id: - return + return None war = self.app.model.get_war_by_campaign( self.app.navigation.selected_campaign_id ) @@ -212,7 +213,7 @@ class CampaignController: self.app.view, default_name="", rounds=rnd_objs, objectives=obj_dtos ) if dialog.exec() != QDialog.DialogCode.Accepted: - return + return None name = dialog.get_sector_name() round_id = dialog.get_round_id() major_id = dialog.get_major_id() @@ -223,8 +224,8 @@ class CampaignController: if not self._validate_sector_inputs( name, round_id, major_id, minor_id, influence_id ): - return - self.app.model.add_sector( + return None + return self.app.model.add_sector( self.app.navigation.selected_campaign_id, name, round_id, @@ -234,8 +235,6 @@ class CampaignController: mission, description, ) - self.app.is_dirty = True - self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) def edit_sector(self, sector_id: str) -> None: sect = self.app.model.get_sector(sector_id) diff --git a/src/warchron/controller/player_controller.py b/src/warchron/controller/player_controller.py index 4767576..ecd27b7 100644 --- a/src/warchron/controller/player_controller.py +++ b/src/warchron/controller/player_controller.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING from PyQt6.QtWidgets import QMessageBox, QDialog from warchron.constants import RefreshScope +from warchron.model.player import Player if TYPE_CHECKING: from warchron.controller.app_controller import AppController @@ -21,16 +22,15 @@ class PlayerController: return False return True - def add_player(self) -> None: + def create_player(self) -> Player | None: dialog = PlayerDialog(self.app.view) - result = dialog.exec() # modal blocking dialog - if result == QDialog.DialogCode.Accepted: - name = dialog.get_player_name() - if not self._validate_player_inputs(name): - return - self.app.model.add_player(name) - self.app.is_dirty = True - self.app.navigation.refresh(RefreshScope.PLAYERS_LIST) + result = dialog.exec() + if result != QDialog.DialogCode.Accepted: + return None + name = dialog.get_player_name() + if not self._validate_player_inputs(name): + return None + return self.app.model.add_player(name) def edit_player(self, player_id: str) -> None: play = self.app.model.get_player(player_id) diff --git a/src/warchron/controller/round_controller.py b/src/warchron/controller/round_controller.py index d5a09e3..93e39ae 100644 --- a/src/warchron/controller/round_controller.py +++ b/src/warchron/controller/round_controller.py @@ -3,6 +3,7 @@ from typing import List, TYPE_CHECKING from PyQt6.QtWidgets import QDialog, QMessageBox from warchron.constants import ItemType, RefreshScope, Icons, IconName +from warchron.model.round import Round if TYPE_CHECKING: from warchron.controller.app_controller import AppController @@ -111,14 +112,11 @@ class RoundController: self.app.view.display_round_battles(battles_for_display) self.app.view.endRoundBtn.setEnabled(not rnd.is_over) - def add_round(self) -> None: - if not self.app.navigation.selected_campaign_id: - return - rnd = self.app.model.add_round(self.app.navigation.selected_campaign_id) - self.app.is_dirty = True - self.app.navigation.refresh_and_select( - RefreshScope.WARS_TREE, item_type=ItemType.ROUND, item_id=rnd.id - ) + def create_round(self) -> Round | None: + campaign_id = self.app.navigation.selected_campaign_id + if not campaign_id: + return None + return self.app.model.add_round(campaign_id) def close_round(self) -> None: round_id = self.app.navigation.selected_round_id diff --git a/src/warchron/controller/war_controller.py b/src/warchron/controller/war_controller.py index f360b8b..8702bf8 100644 --- a/src/warchron/controller/war_controller.py +++ b/src/warchron/controller/war_controller.py @@ -2,7 +2,7 @@ from typing import List, TYPE_CHECKING from PyQt6.QtWidgets import QMessageBox, QDialog -from warchron.constants import ItemType, RefreshScope +from warchron.constants import RefreshScope if TYPE_CHECKING: from warchron.controller.app_controller import AppController @@ -11,6 +11,9 @@ from warchron.controller.dtos import ( WarParticipantDTO, ObjectiveDTO, ) +from warchron.model.war import War +from warchron.model.war_participant import WarParticipant +from warchron.model.objective import Objective from warchron.model.closure_service import ClosureService from warchron.view.war_dialog import WarDialog from warchron.view.objective_dialog import ObjectiveDialog @@ -60,21 +63,18 @@ class WarController: return False return True - def add_war(self) -> None: + def create_war(self) -> War | None: dialog = WarDialog( self.app.view, default_year=self.app.model.get_default_war_values()["year"] ) - result = dialog.exec() # modal blocking dialog - if result == QDialog.DialogCode.Accepted: - name = dialog.get_war_name() - year = dialog.get_war_year() - if not self._validate_war_inputs(name, year): - return - war = self.app.model.add_war(name, year) - self.app.is_dirty = True - self.app.navigation.refresh_and_select( - RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war.id - ) + result = dialog.exec() + if result != QDialog.DialogCode.Accepted: + return None + name = dialog.get_war_name() + year = dialog.get_war_year() + if not self._validate_war_inputs(name, year): + return None + return self.app.model.add_war(name, year) def edit_war(self, war_id: str) -> None: war = self.app.model.get_war(war_id) @@ -142,21 +142,19 @@ class WarController: return False return True - def add_objective(self) -> None: + def create_objective(self) -> Objective | None: if not self.app.navigation.selected_war_id: - return + return None dialog = ObjectiveDialog(self.app.view) if dialog.exec() != QDialog.DialogCode.Accepted: - return + return None name = dialog.get_objective_name() description = dialog.get_objective_description() if not self._validate_objective_inputs(name, description): - return - self.app.model.add_objective( + return None + return self.app.model.add_objective( self.app.navigation.selected_war_id, name, description ) - self.app.is_dirty = True - self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) def edit_objective(self, objective_id: str) -> None: obj = self.app.model.get_objective(objective_id) @@ -174,9 +172,9 @@ class WarController: # War participant methods - def add_war_participant(self) -> None: + def create_war_participant(self) -> WarParticipant | None: if not self.app.navigation.selected_war_id: - return + return None players = self.app.model.get_available_players( self.app.navigation.selected_war_id ) @@ -185,16 +183,14 @@ class WarController: ] dialog = WarParticipantDialog(self.app.view, players=play_opts) if dialog.exec() != QDialog.DialogCode.Accepted: - return + return None player_id = dialog.get_player_id() faction = dialog.get_participant_faction() if not player_id: - return - self.app.model.add_war_participant( + return None + return self.app.model.add_war_participant( self.app.navigation.selected_war_id, player_id, faction ) - self.app.is_dirty = True - self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) def edit_war_participant(self, participant_id: str) -> None: war_part = self.app.model.get_war_participant(participant_id) diff --git a/src/warchron/model/closure_workflow.py b/src/warchron/model/closure_workflow.py new file mode 100644 index 0000000..db7ee66 --- /dev/null +++ b/src/warchron/model/closure_workflow.py @@ -0,0 +1,19 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from warchron.model.war import War + from warchron.model.war import War + from warchron.model.war import War + from warchron.model.closure_service import ClosureService + + +class RoundClosureWorkflow: + + def close_round(self, round_id): + rnd = repo.get_round(round_id) + + ties = ClosureService.close_round(rnd) + + repo.save() + + return ties diff --git a/src/warchron/model/model.py b/src/warchron/model/model.py index b5eeaf3..4519f07 100644 --- a/src/warchron/model/model.py +++ b/src/warchron/model/model.py @@ -159,6 +159,8 @@ class Model: def update_war(self, war_id: str, *, name: str, year: int) -> None: war = self.get_war(war_id) + if war.is_over: + raise ForbiddenOperation("Can't update a closed war.") war.set_name(name) war.set_year(year) diff --git a/src/warchron/model/score_service.py b/src/warchron/model/score_service.py index 30bb48e..2bfda9b 100644 --- a/src/warchron/model/score_service.py +++ b/src/warchron/model/score_service.py @@ -37,3 +37,9 @@ class ScoreService: if sector.minor_objective_id: totals[sector.minor_objective_id] += war.minor_value return totals + + # def compute_round_results(round) + + # def compute_campaign_winner(campaign) + + # def compute_war_winner(war) diff --git a/src/warchron/model/war.py b/src/warchron/model/war.py index cacf5a8..3ff19b5 100644 --- a/src/warchron/model/war.py +++ b/src/warchron/model/war.py @@ -238,6 +238,8 @@ class War: if self.is_over: raise ForbiddenOperation("Can't update campaign in a closed war.") camp = self.get_campaign(campaign_id) + if camp.is_over: + raise ForbiddenOperation("Can't update a closed campaign.") camp.set_name(name) camp.set_month(month) diff --git a/src/warchron/view/view.py b/src/warchron/view/view.py index d4aed89..5950dae 100644 --- a/src/warchron/view/view.py +++ b/src/warchron/view/view.py @@ -41,8 +41,7 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): self.majorValue.setMinimum(0) self.minorValue.setMinimum(0) self.on_influence_token_changed: Callable[[int], None] | None = None - self.on_add_campaign: Callable[[], None] | None = None - self.on_add_round: Callable[[], None] | None = None + self.on_add_item: Callable[[str], None] | None = None self.on_edit_item: Callable[[str, str], None] | None = None self.on_delete_item: Callable[[str, str], None] | None = None self.splitter.setSizes([200, 800]) @@ -202,12 +201,12 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): # Wars view def _on_add_campaign_clicked(self) -> None: - if self.on_add_campaign: - self.on_add_campaign() + if self.on_add_item: + self.on_add_item(ItemType.CAMPAIGN) def _on_add_round_clicked(self) -> None: - if self.on_add_round: - self.on_add_round() + if self.on_add_item: + self.on_add_item(ItemType.ROUND) def set_add_campaign_enabled(self, enabled: bool) -> None: self.addCampaignBtn.setEnabled(enabled)