diff --git a/src/warchron/controller/app_controller.py b/src/warchron/controller/app_controller.py index 7ac6d7c..e4356e8 100644 --- a/src/warchron/controller/app_controller.py +++ b/src/warchron/controller/app_controller.py @@ -3,7 +3,11 @@ from pathlib import Path from PyQt6.QtWidgets import QMessageBox from warchron.model.model import Model -from warchron.model.exception import DomainError, RequiresConfirmation +from warchron.model.exception import ( + DeletionForbidden, + DeletionRequiresConfirmation, + UpdateRequiresConfirmation, +) from warchron.view.view import View from warchron.constants import ItemType, RefreshScope from warchron.controller.navigation_controller import NavigationController @@ -31,7 +35,8 @@ 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_item = self.add_item + self.view.on_add_campaign = self.campaigns.add_campaign + self.view.on_add_round = self.rounds.add_round def __connect(self) -> None: self.view.actionExit.triggered.connect(self.view.close) @@ -40,25 +45,20 @@ 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(lambda: self.add_item(ItemType.PLAYER)) - self.view.addWarBtn.clicked.connect(lambda: self.add_item(ItemType.WAR)) + self.view.addPlayerBtn.clicked.connect(self.players.add_player) + self.view.addWarBtn.clicked.connect(self.wars.add_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( - lambda: self.add_item(ItemType.OBJECTIVE) - ) - self.view.addWarParticipantBtn.clicked.connect( - lambda: self.add_item(ItemType.WAR_PARTICIPANT) - ) + self.view.addObjectiveBtn.clicked.connect(self.wars.add_objective) + self.view.addWarParticipantBtn.clicked.connect(self.wars.add_war_participant) self.view.endWarBtn.clicked.connect(self.wars.close_war) - self.view.addSectorBtn.clicked.connect(lambda: self.add_item(ItemType.SECTOR)) + self.view.addSectorBtn.clicked.connect(self.campaigns.add_sector) self.view.addCampaignParticipantBtn.clicked.connect( - lambda: self.add_item(ItemType.CAMPAIGN_PARTICIPANT) + self.campaigns.add_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 @@ -165,72 +165,6 @@ 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: @@ -265,21 +199,15 @@ 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: + except UpdateRequiresConfirmation as e: reply = QMessageBox.question( self.view, "Confirm update", - str(e), + e.message, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) if reply == QMessageBox.StandardButton.Yes: - e.action() + e.apply_update() self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) def delete_item(self, item_type: str, item_id: str) -> None: @@ -325,19 +253,19 @@ class AppController: RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp_id ) self.is_dirty = True - except DomainError as e: + except DeletionForbidden as e: QMessageBox.warning( self.view, "Deletion forbidden", - str(e), + e.reason, ) - except RequiresConfirmation as e: + except DeletionRequiresConfirmation as e: reply = QMessageBox.question( self.view, "Confirm deletion", - str(e), + e.message, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) if reply == QMessageBox.StandardButton.Yes: - e.action() + e.cleanup_action() self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) diff --git a/src/warchron/controller/campaign_controller.py b/src/warchron/controller/campaign_controller.py index 022fd54..f5ff268 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 RefreshScope +from warchron.constants import ItemType, RefreshScope if TYPE_CHECKING: from warchron.controller.app_controller import AppController @@ -13,9 +13,6 @@ 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 @@ -71,9 +68,9 @@ class CampaignController: return False return True - def create_campaign(self) -> Campaign | None: + def add_campaign(self) -> None: if not self.app.navigation.selected_war_id: - return None + return dialog = CampaignDialog( self.app.view, default_month=self.app.model.get_default_campaign_values( @@ -81,18 +78,18 @@ class CampaignController: )["month"], ) if dialog.exec() != QDialog.DialogCode.Accepted: - return None + return name = dialog.get_campaign_name() month = dialog.get_campaign_month() if not self._validate_campaign_inputs(name, month): - return None - return self.app.model.add_campaign( + return + camp = 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) @@ -131,9 +128,9 @@ class CampaignController: # Campaign participant methods - def create_campaign_participant(self) -> CampaignParticipant | None: + def add_campaign_participant(self) -> None: if not self.app.navigation.selected_campaign_id: - return None + return participants = self.app.model.get_available_war_participants( self.app.navigation.selected_campaign_id ) @@ -143,15 +140,17 @@ class CampaignController: ] dialog = CampaignParticipantDialog(self.app.view, participants=part_opts) if dialog.exec() != QDialog.DialogCode.Accepted: - return None + return player_id = dialog.get_player_id() leader = dialog.get_participant_leader() theme = dialog.get_participant_theme() if not player_id: - return None - return self.app.model.add_campaign_participant( + 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) @@ -192,9 +191,9 @@ class CampaignController: # allow same objectives in different fields? return True - def create_sector(self) -> Sector | None: + def add_sector(self) -> None: if not self.app.navigation.selected_campaign_id: - return None + return war = self.app.model.get_war_by_campaign( self.app.navigation.selected_campaign_id ) @@ -213,7 +212,7 @@ class CampaignController: self.app.view, default_name="", rounds=rnd_objs, objectives=obj_dtos ) if dialog.exec() != QDialog.DialogCode.Accepted: - return None + return name = dialog.get_sector_name() round_id = dialog.get_round_id() major_id = dialog.get_major_id() @@ -224,8 +223,8 @@ class CampaignController: if not self._validate_sector_inputs( name, round_id, major_id, minor_id, influence_id ): - return None - return self.app.model.add_sector( + return + self.app.model.add_sector( self.app.navigation.selected_campaign_id, name, round_id, @@ -235,6 +234,8 @@ 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 ecd27b7..4767576 100644 --- a/src/warchron/controller/player_controller.py +++ b/src/warchron/controller/player_controller.py @@ -3,7 +3,6 @@ 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 @@ -22,15 +21,16 @@ class PlayerController: return False return True - def create_player(self) -> Player | None: + def add_player(self) -> None: dialog = PlayerDialog(self.app.view) - 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) + 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) 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 93e39ae..fcca17e 100644 --- a/src/warchron/controller/round_controller.py +++ b/src/warchron/controller/round_controller.py @@ -3,7 +3,6 @@ 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 @@ -112,11 +111,14 @@ class RoundController: self.app.view.display_round_battles(battles_for_display) self.app.view.endRoundBtn.setEnabled(not rnd.is_over) - 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 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 close_round(self) -> None: round_id = self.app.navigation.selected_round_id @@ -139,9 +141,7 @@ class RoundController: return self.app.is_dirty = True self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) - self.app.navigation.refresh_and_select( - RefreshScope.WARS_TREE, item_type=ItemType.ROUND, item_id=round_id - ) + self.app.navigation.refresh(RefreshScope.WARS_TREE) # Choice methods diff --git a/src/warchron/controller/war_controller.py b/src/warchron/controller/war_controller.py index 9851333..f360b8b 100644 --- a/src/warchron/controller/war_controller.py +++ b/src/warchron/controller/war_controller.py @@ -2,8 +2,7 @@ from typing import List, TYPE_CHECKING from PyQt6.QtWidgets import QMessageBox, QDialog -from warchron.constants import RefreshScope -from warchron.model.exception import DomainError +from warchron.constants import ItemType, RefreshScope if TYPE_CHECKING: from warchron.controller.app_controller import AppController @@ -12,9 +11,6 @@ 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 @@ -64,18 +60,21 @@ class WarController: return False return True - def create_war(self) -> War | None: + def add_war(self) -> None: dialog = WarDialog( self.app.view, default_year=self.app.model.get_default_war_values()["year"] ) - 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) + 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 + ) def edit_war(self, war_id: str) -> None: war = self.app.model.get_war(war_id) @@ -116,46 +115,22 @@ class WarController: war_id = self.app.navigation.selected_war_id if not war_id: return - try: - self.app.model.set_major_value(war_id, value) - except DomainError as e: - QMessageBox.warning( - self.app.view, - "Setting forbidden", - str(e), - ) + self.app.model.set_major_value(war_id, value) self.app.is_dirty = True - self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) def set_minor_value(self, value: int) -> None: war_id = self.app.navigation.selected_war_id if not war_id: return - try: - self.app.model.set_minor_value(war_id, value) - except DomainError as e: - QMessageBox.warning( - self.app.view, - "Setting forbidden", - str(e), - ) + self.app.model.set_minor_value(war_id, value) self.app.is_dirty = True - self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) def set_influence_token(self, checked: bool) -> None: war_id = self.app.navigation.selected_war_id if not war_id: return - try: - self.app.model.set_influence_token(war_id, checked) - except DomainError as e: - QMessageBox.warning( - self.app.view, - "Setting forbidden", - str(e), - ) + self.app.model.set_influence_token(war_id, checked) self.app.is_dirty = True - self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) # Objective methods @@ -167,19 +142,21 @@ class WarController: return False return True - def create_objective(self) -> Objective | None: + def add_objective(self) -> None: if not self.app.navigation.selected_war_id: - return None + return dialog = ObjectiveDialog(self.app.view) if dialog.exec() != QDialog.DialogCode.Accepted: - return None + return name = dialog.get_objective_name() description = dialog.get_objective_description() if not self._validate_objective_inputs(name, description): - return None - return self.app.model.add_objective( + 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) @@ -197,9 +174,9 @@ class WarController: # War participant methods - def create_war_participant(self) -> WarParticipant | None: + def add_war_participant(self) -> None: if not self.app.navigation.selected_war_id: - return None + return players = self.app.model.get_available_players( self.app.navigation.selected_war_id ) @@ -208,14 +185,16 @@ class WarController: ] dialog = WarParticipantDialog(self.app.view, players=play_opts) if dialog.exec() != QDialog.DialogCode.Accepted: - return None + return player_id = dialog.get_player_id() faction = dialog.get_participant_faction() if not player_id: - return None - return self.app.model.add_war_participant( + 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/campaign.py b/src/warchron/model/campaign.py index 2b8148a..fb16c97 100644 --- a/src/warchron/model/campaign.py +++ b/src/warchron/model/campaign.py @@ -2,7 +2,10 @@ from __future__ import annotations from uuid import uuid4 from typing import Any, Dict, List -from warchron.model.exception import ForbiddenOperation, RequiresConfirmation +from warchron.model.exception import ( + DeletionRequiresConfirmation, + UpdateRequiresConfirmation, +) from warchron.model.campaign_participant import CampaignParticipant from warchron.model.sector import Sector from warchron.model.round import Round @@ -75,8 +78,6 @@ class Campaign: def add_campaign_participant( self, war_participant_id: str, leader: str, theme: str ) -> CampaignParticipant: - if self.is_over: - raise ForbiddenOperation("Can't add participant in a closed campaign.") if self.has_war_participant(war_participant_id): raise ValueError("Player already registered in this campaign") participant = CampaignParticipant( @@ -97,16 +98,12 @@ class Campaign: def update_campaign_participant( self, participant_id: str, *, leader: str, theme: str ) -> None: - if self.is_over: - raise ForbiddenOperation("Can't update participant in a closed campaign.") part = self.get_campaign_participant(participant_id) # Can't change referred War.participant part.set_leader(leader) part.set_theme(theme) def remove_campaign_participant(self, participant_id: str) -> None: - if self.is_over: - raise ForbiddenOperation("Can't remove participant in a closed campaign.") rounds_blocking: list[Round] = [] for rnd in self.rounds: if rnd.has_choice_with_participant( @@ -126,11 +123,13 @@ class Campaign: rounds_str = ", ".join( str(self.get_round_index(rnd.id)) for rnd in rounds_blocking ) - raise RequiresConfirmation( - f"This participant is used in round(s): {rounds_str}.\n" - "Related choices and battles will be cleared.\n" - "Do you want to continue?", - action=cleanup, + raise DeletionRequiresConfirmation( + message=( + f"This participant is used in round(s): {rounds_str}.\n" + "Related choices and battles will be cleared.\n" + "Do you want to continue?" + ), + cleanup_action=cleanup, ) # Sector methods @@ -156,8 +155,6 @@ class Campaign: mission: str | None, description: str | None, ) -> Sector: - if self.is_over: - raise ForbiddenOperation("Can't add sector in a closed campaign.") sect = Sector( name, round_id, major_id, minor_id, influence_id, mission, description ) @@ -187,8 +184,6 @@ class Campaign: mission: str | None, description: str | None, ) -> None: - if self.is_over: - raise ForbiddenOperation("Can't update sector in a closed campaign.") sect = self.get_sector(sector_id) old_round_id = sect.round_id @@ -224,16 +219,16 @@ class Campaign: rounds_str = ", ".join( str(self.get_round_index(rnd.id)) for rnd in affected_rounds ) - raise RequiresConfirmation( - f"Changing the round of this sector will affect round(s): {rounds_str}." - "\nRelated battles and choices will be cleared.\n" - "Do you want to continue?", - action=cleanup_and_update, + raise UpdateRequiresConfirmation( + message=( + f"Changing the round of this sector will affect round(s): {rounds_str}." + "\nRelated battles and choices will be cleared.\n" + "Do you want to continue?" + ), + apply_update=cleanup_and_update, ) def remove_sector(self, sector_id: str) -> None: - if self.is_over: - raise ForbiddenOperation("Can't remove sector in a closed campaign.") rounds_blocking: list[Round] = [] for rnd in self.rounds: if rnd.has_battle_with_sector(sector_id) or rnd.has_choice_with_sector( @@ -253,11 +248,13 @@ class Campaign: rounds_str = ", ".join( str(self.get_round_index(rnd.id)) for rnd in rounds_blocking ) - raise RequiresConfirmation( - f"This sector is used in round(s): {rounds_str}.\n" - "Battles and choices using this sector will be cleared.\n" - "Do you want to continue?", - action=cleanup, + raise DeletionRequiresConfirmation( + message=( + f"This sector is used in round(s): {rounds_str}.\n" + "Battles and choices using this sector will be cleared.\n" + "Do you want to continue?" + ), + cleanup_action=cleanup, ) def get_sectors_in_round(self, round_id: str) -> List[Sector]: @@ -269,15 +266,6 @@ class Campaign: def has_round(self, round_id: str) -> bool: return any(r.id == round_id for r in self.rounds) - def has_finished_round(self) -> bool: - return any(r.is_over for r in self.rounds) - - def has_finished_battle(self) -> bool: - return any(r.has_finished_battle() for r in self.rounds) - - def all_rounds_finished(self) -> bool: - return all(r.is_over for r in self.rounds) - def get_round(self, round_id: str) -> Round: for rnd in self.rounds: if rnd.id == round_id: @@ -288,21 +276,12 @@ class Campaign: return list(self.rounds) def add_round(self) -> Round: - if self.is_over: - raise ForbiddenOperation("Can't add round in a closed campaign.") round = Round() self.rounds.append(round) return round def remove_round(self, round_id: str) -> None: - if self.is_over: - raise ForbiddenOperation("Can't remove round in a closed campaign.") rnd = next((r for r in self.rounds if r.id == round_id), None) - if rnd: - if rnd.is_over: - raise ForbiddenOperation("Can't remove closed round.") - if rnd.has_finished_battle(): - raise ForbiddenOperation("Can't remove round with finished battle(s).") for sect in self.sectors.values(): if sect.round_id == round_id: sect.round_id = None @@ -342,6 +321,9 @@ class Campaign: # Battle methods + def all_rounds_finished(self) -> bool: + return all(r.is_over for r in self.rounds) + def create_battle(self, round_id: str, sector_id: str) -> Battle: rnd = self.get_round(round_id) return rnd.create_battle(sector_id) diff --git a/src/warchron/model/closure_workflow.py b/src/warchron/model/closure_workflow.py deleted file mode 100644 index db7ee66..0000000 --- a/src/warchron/model/closure_workflow.py +++ /dev/null @@ -1,19 +0,0 @@ -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/exception.py b/src/warchron/model/exception.py index 0c6c38e..be14e13 100644 --- a/src/warchron/model/exception.py +++ b/src/warchron/model/exception.py @@ -1,25 +1,31 @@ from typing import Callable -class DomainError(Exception): - """Base class for all domain rule violations.""" - - pass +class DeletionForbidden(Exception): + def __init__(self, reason: str): + self.reason = reason + super().__init__(reason) -class ForbiddenOperation(DomainError): - """Generic 'you can't do this' rule.""" - - pass - - -class DomainDecision(Exception): - """Base class for domain actions requiring user decision.""" - - pass - - -class RequiresConfirmation(DomainDecision): - def __init__(self, message: str, action: Callable[[], None]): +class DeletionRequiresConfirmation(Exception): + def __init__( + self, + message: str, + *, + cleanup_action: Callable[[], None], + ): + self.message = message + self.cleanup_action = cleanup_action + super().__init__(message) + + +class UpdateRequiresConfirmation(Exception): + def __init__( + self, + message: str, + *, + apply_update: Callable[[], None], + ): + self.message = message + self.apply_update = apply_update super().__init__(message) - self.action = action diff --git a/src/warchron/model/model.py b/src/warchron/model/model.py index 65a4a7b..ca7cd10 100644 --- a/src/warchron/model/model.py +++ b/src/warchron/model/model.py @@ -4,7 +4,7 @@ import json import shutil from datetime import datetime -from warchron.model.exception import ForbiddenOperation +from warchron.model.exception import DeletionForbidden from warchron.model.player import Player from warchron.model.war import War from warchron.model.war_participant import WarParticipant @@ -91,7 +91,7 @@ class Model: wars_using_player.append(war.name) if wars_using_player: wars_str = ", ".join(wars_using_player) - raise ForbiddenOperation( + raise DeletionForbidden( f"This player is participating in war(s): {wars_str}.\n" "Remove it from participants first." ) @@ -159,42 +159,25 @@ 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) def set_major_value(self, war_id: str, value: int) -> None: war = self.get_war(war_id) - war.set_major_value(value) + war.set_major(value) def set_minor_value(self, war_id: str, value: int) -> None: war = self.get_war(war_id) - war.set_minor_value(value) + war.set_minor(value) def set_influence_token(self, war_id: str, value: bool) -> None: war = self.get_war(war_id) - war.set_influence_token(value) + war.set_influence(value) def get_all_wars(self) -> List[War]: return list(self.wars.values()) def remove_war(self, war_id: str) -> None: - war = self.wars[war_id] - if war: - if war.is_over: - raise ForbiddenOperation("Can't remove closed war.") - if war.has_finished_campaign(): - raise ForbiddenOperation("Can't remove war with finished campaign(s).") - if war.has_finished_round(): - raise ForbiddenOperation( - "Can't remove war with finished round(s) in campaign(s)." - ) - if war.has_finished_battle(): - raise ForbiddenOperation( - "Can't remove war with finished battle(s) in round(s) " - "in campaign(s)." - ) del self.wars[war_id] # Objective methods diff --git a/src/warchron/model/round.py b/src/warchron/model/round.py index 08d77d6..cb15f20 100644 --- a/src/warchron/model/round.py +++ b/src/warchron/model/round.py @@ -2,7 +2,6 @@ from __future__ import annotations from uuid import uuid4 from typing import Any, Dict -from warchron.model.exception import ForbiddenOperation from warchron.model.choice import Choice from warchron.model.battle import Battle @@ -59,8 +58,6 @@ class Round: ) def create_choice(self, participant_id: str) -> Choice: - if self.is_over: - raise ForbiddenOperation("Can't create choice in a closed round.") if participant_id not in self.choices: choice = Choice( participant_id=participant_id, @@ -77,8 +74,6 @@ class Round: secondary_sector_id: str | None, comment: str | None, ) -> None: - if self.is_over: - raise ForbiddenOperation("Can't update choice in a closed round.") choice = self.get_choice(participant_id) if choice: choice.set_priority(priority_sector_id) @@ -93,8 +88,6 @@ class Round: choice.secondary_sector_id = None def remove_choice(self, participant_id: str) -> None: - if self.is_over: - raise ForbiddenOperation("Can't remove choice in a closed round.") del self.choices[participant_id] # Battle methods @@ -111,17 +104,12 @@ class Round: for bat in self.battles.values() ) - def has_finished_battle(self) -> bool: - return any(b.is_finished() for b in self.battles.values()) - def all_battles_finished(self) -> bool: return all( b.winner_id is not None or b.is_draw() for b in self.battles.values() ) def create_battle(self, sector_id: str) -> Battle: - if self.is_over: - raise ForbiddenOperation("Can't create battle in a closed round.") if sector_id not in self.battles: battle = Battle(sector_id=sector_id, player_1_id=None, player_2_id=None) self.battles[sector_id] = battle @@ -137,8 +125,6 @@ class Round: victory_condition: str | None, comment: str | None, ) -> None: - if self.is_over: - raise ForbiddenOperation("Can't update battle in a closed round.") bat = self.get_battle(sector_id) if bat: bat.set_player_1(player_1_id) @@ -158,9 +144,4 @@ class Round: battle.winner_id = None def remove_battle(self, sector_id: str) -> None: - if self.is_over: - raise ForbiddenOperation("Can't remove battle in a closed round.") - bat = self.battles[sector_id] - if bat and bat.is_finished(): - raise ForbiddenOperation("Can't remove finished battle.") del self.battles[sector_id] diff --git a/src/warchron/model/score_service.py b/src/warchron/model/score_service.py index 2bfda9b..30bb48e 100644 --- a/src/warchron/model/score_service.py +++ b/src/warchron/model/score_service.py @@ -37,9 +37,3 @@ 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 ca7c6ff..a2fa125 100644 --- a/src/warchron/model/war.py +++ b/src/warchron/model/war.py @@ -3,7 +3,7 @@ from uuid import uuid4 from datetime import datetime from typing import Any, Dict, List -from warchron.model.exception import ForbiddenOperation +from warchron.model.exception import DeletionForbidden from warchron.model.war_participant import WarParticipant from warchron.model.objective import Objective from warchron.model.campaign_participant import CampaignParticipant @@ -36,23 +36,17 @@ class War: def set_year(self, new_year: int) -> None: self.year = new_year - def set_major_value(self, new_value: int) -> None: - if self.is_over: - raise ForbiddenOperation("Can't set major value of a closed war.") + def set_major(self, new_value: int) -> None: if new_value < self.minor_value: - raise ValueError("Can' set major value < minor value") + raise ValueError("major_value cannot be < minor_value") self.major_value = new_value - def set_minor_value(self, new_value: int) -> None: - if self.is_over: - raise ForbiddenOperation("Can't set minor value of a closed war.") + def set_minor(self, new_value: int) -> None: if new_value > self.major_value: - raise ValueError("Can't set minor value > major value") + raise ValueError("minor_value cannot be > major_value") self.minor_value = new_value - def set_influence_token(self, new_state: bool) -> None: - if self.is_over: - raise ForbiddenOperation("Can't set influence token of a closed war.") + def set_influence(self, new_state: bool) -> None: self.influence_token = new_state def set_state(self, new_state: bool) -> None: @@ -93,8 +87,6 @@ class War: # Objective methods def add_objective(self, name: str, description: str | None) -> Objective: - if self.is_over: - raise ForbiddenOperation("Can't add objective in a closed war.") obj = Objective(name, description) self.objectives[obj.id] = obj return obj @@ -114,22 +106,18 @@ class War: def update_objective( self, objective_id: str, *, name: str, description: str | None ) -> None: - if self.is_over: - raise ForbiddenOperation("Can't update objective in a closed war.") obj = self.get_objective(objective_id) obj.set_name(name) obj.set_description(description) def remove_objective(self, objective_id: str) -> None: - if self.is_over: - raise ForbiddenOperation("Can't remove objective in a closed war.") camp_using_obj: List[str] = [] for camp in self.campaigns: if camp.has_sector_with_objective(objective_id): camp_using_obj.append(camp.name) if camp_using_obj: camps_str = ", ".join(camp_using_obj) - raise ForbiddenOperation( + raise DeletionForbidden( f"This objective is used in campaign(s) sector(s): {camps_str}.\n" "Remove it from campaign(s) first." ) @@ -147,8 +135,6 @@ class War: return any(part.player_id == player_id for part in self.participants.values()) def add_war_participant(self, player_id: str, faction: str) -> WarParticipant: - if self.is_over: - raise ForbiddenOperation("Can't add participant in a closed war.") if self.has_player(player_id): raise ValueError("Player already registered in this war") participant = WarParticipant(player_id=player_id, faction=faction) @@ -162,22 +148,18 @@ class War: return list(self.participants.values()) def update_war_participant(self, participant_id: str, *, faction: str) -> None: - if self.is_over: - raise ForbiddenOperation("Can't update participant in a closed war.") part = self.get_war_participant(participant_id) # Can't change referred Model.players part.set_faction(faction) def remove_war_participant(self, participant_id: str) -> None: - if self.is_over: - raise ForbiddenOperation("Can't remove participant in a closed war.") camp_using_part: List[str] = [] for camp in self.campaigns: if camp.has_war_participant(participant_id): camp_using_part.append(camp.name) if camp_using_part: camps_str = ", ".join(camp_using_part) - raise ForbiddenOperation( + raise DeletionForbidden( f"This war participant is used in campaign(s): {camps_str}.\n" "Remove it from campaign(s) first." ) @@ -191,21 +173,10 @@ class War: def get_default_campaign_values(self) -> Dict[str, Any]: return {"month": datetime.now().month} - def has_finished_campaign(self) -> bool: - return any(c.is_over for c in self.campaigns) - - def has_finished_round(self) -> bool: - return any(c.has_finished_round() for c in self.campaigns) - - def has_finished_battle(self) -> bool: - return any(c.has_finished_battle() for c in self.campaigns) - def all_campaigns_finished(self) -> bool: return all(c.is_over for c in self.campaigns) def add_campaign(self, name: str, month: int | None = None) -> Campaign: - if self.is_over: - raise ForbiddenOperation("Can't add campaign in a closed war.") if month is None: month = self.get_default_campaign_values()["month"] campaign = Campaign(name, month) @@ -241,11 +212,7 @@ class War: raise KeyError(f"Participant {participant_id} not found in any Campaign") def update_campaign(self, campaign_id: str, *, name: str, month: int) -> None: - 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) @@ -253,20 +220,7 @@ class War: return list(self.campaigns) def remove_campaign(self, campaign_id: str) -> None: - if self.is_over: - raise ForbiddenOperation("Can't remove campaign in a closed war.") camp = self.get_campaign(campaign_id) - if camp: - if camp.is_over: - raise ForbiddenOperation("Can't remove closed campaign.") - if camp.has_finished_round(): - raise ForbiddenOperation( - "Can't remove campaign with finished round(s)." - ) - if camp.has_finished_battle(): - raise ForbiddenOperation( - "Can't remove campaign with finished battle(s) in round(s)." - ) self.campaigns.remove(camp) # Sector methods diff --git a/src/warchron/view/view.py b/src/warchron/view/view.py index 5950dae..caf73ae 100644 --- a/src/warchron/view/view.py +++ b/src/warchron/view/view.py @@ -41,7 +41,8 @@ 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_item: Callable[[str], None] | None = None + self.on_add_campaign: Callable[[], None] | None = None + self.on_add_round: Callable[[], 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]) @@ -201,12 +202,12 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): # Wars view def _on_add_campaign_clicked(self) -> None: - if self.on_add_item: - self.on_add_item(ItemType.CAMPAIGN) + if self.on_add_campaign: + self.on_add_campaign() def _on_add_round_clicked(self) -> None: - if self.on_add_item: - self.on_add_item(ItemType.ROUND) + if self.on_add_round: + self.on_add_round() def set_add_campaign_enabled(self, enabled: bool) -> None: self.addCampaignBtn.setEnabled(enabled) @@ -237,10 +238,6 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): def display_wars_tree(self, wars: List[WarDTO]) -> None: tree = self.warsTree - try: - tree.currentItemChanged.disconnect() - except TypeError: - pass tree.clear() tree.setColumnCount(1) tree.setHeaderLabels(["Wars"])