exceptions adding in closed elements

This commit is contained in:
Maxime Réaux 2026-02-13 15:44:28 +01:00
parent 88bd28e949
commit a2b6c7c684
10 changed files with 179 additions and 82 deletions

View file

@ -31,8 +31,7 @@ class AppController:
self.navigation.refresh_wars_view() self.navigation.refresh_wars_view()
self.update_window_title() self.update_window_title()
self.view.on_tree_selection_changed = self.navigation.on_tree_selection_changed 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_item = self.add_item
self.view.on_add_round = self.rounds.add_round
def __connect(self) -> None: def __connect(self) -> None:
self.view.actionExit.triggered.connect(self.view.close) self.view.actionExit.triggered.connect(self.view.close)
@ -41,20 +40,25 @@ class AppController:
self.view.actionSave.triggered.connect(self.save) self.view.actionSave.triggered.connect(self.save)
self.view.actionSave_as.triggered.connect(self.save_as) self.view.actionSave_as.triggered.connect(self.save_as)
self.view.actionAbout.triggered.connect(self.show_about) self.view.actionAbout.triggered.connect(self.show_about)
self.view.addPlayerBtn.clicked.connect(self.players.add_player) self.view.addPlayerBtn.clicked.connect(lambda: self.add_item(ItemType.PLAYER))
self.view.addWarBtn.clicked.connect(self.wars.add_war) self.view.addWarBtn.clicked.connect(lambda: self.add_item(ItemType.WAR))
self.view.majorValue.valueChanged.connect(self.wars.set_major_value) self.view.majorValue.valueChanged.connect(self.wars.set_major_value)
self.view.minorValue.valueChanged.connect(self.wars.set_minor_value) self.view.minorValue.valueChanged.connect(self.wars.set_minor_value)
self.view.influenceToken.toggled.connect(self.wars.set_influence_token) self.view.influenceToken.toggled.connect(self.wars.set_influence_token)
self.view.addObjectiveBtn.clicked.connect(self.wars.add_objective) self.view.addObjectiveBtn.clicked.connect(
self.view.addWarParticipantBtn.clicked.connect(self.wars.add_war_participant) 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.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.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.endCampaignBtn.clicked.connect(self.campaigns.close_campaign)
self.view.endRoundBtn.clicked.connect(self.rounds.close_round) 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_edit_item = self.edit_item
self.view.on_delete_item = self.delete_item self.view.on_delete_item = self.delete_item
@ -161,6 +165,72 @@ class AppController:
# Command methods # 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: def edit_item(self, item_type: str, item_id: str) -> None:
try: try:
if item_type == ItemType.PLAYER: if item_type == ItemType.PLAYER:
@ -195,6 +265,12 @@ class AppController:
self.rounds.edit_round_battle(item_id) self.rounds.edit_round_battle(item_id)
self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
self.is_dirty = True self.is_dirty = True
except DomainError as e:
QMessageBox.warning(
self.view,
"Deletion forbidden",
str(e),
)
except RequiresConfirmation as e: except RequiresConfirmation as e:
reply = QMessageBox.question( reply = QMessageBox.question(
self.view, self.view,

View file

@ -2,7 +2,7 @@ from typing import List, TYPE_CHECKING
from PyQt6.QtWidgets import QMessageBox, QDialog from PyQt6.QtWidgets import QMessageBox, QDialog
from warchron.constants import ItemType, RefreshScope from warchron.constants import RefreshScope
if TYPE_CHECKING: if TYPE_CHECKING:
from warchron.controller.app_controller import AppController from warchron.controller.app_controller import AppController
@ -13,6 +13,9 @@ from warchron.controller.dtos import (
SectorDTO, SectorDTO,
RoundDTO, 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.model.closure_service import ClosureService
from warchron.view.campaign_dialog import CampaignDialog from warchron.view.campaign_dialog import CampaignDialog
from warchron.view.campaign_participant_dialog import CampaignParticipantDialog from warchron.view.campaign_participant_dialog import CampaignParticipantDialog
@ -68,9 +71,9 @@ class CampaignController:
return False return False
return True return True
def add_campaign(self) -> None: def create_campaign(self) -> Campaign | None:
if not self.app.navigation.selected_war_id: if not self.app.navigation.selected_war_id:
return return None
dialog = CampaignDialog( dialog = CampaignDialog(
self.app.view, self.app.view,
default_month=self.app.model.get_default_campaign_values( default_month=self.app.model.get_default_campaign_values(
@ -78,18 +81,18 @@ class CampaignController:
)["month"], )["month"],
) )
if dialog.exec() != QDialog.DialogCode.Accepted: if dialog.exec() != QDialog.DialogCode.Accepted:
return return None
name = dialog.get_campaign_name() name = dialog.get_campaign_name()
month = dialog.get_campaign_month() month = dialog.get_campaign_month()
if not self._validate_campaign_inputs(name, month): if not self._validate_campaign_inputs(name, month):
return return None
camp = self.app.model.add_campaign( return self.app.model.add_campaign(
self.app.navigation.selected_war_id, name, month self.app.navigation.selected_war_id, name, month
) )
self.app.is_dirty = True # self.app.is_dirty = True
self.app.navigation.refresh_and_select( # self.app.navigation.refresh_and_select(
RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp.id # RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp.id
) # )
def edit_campaign(self, campaign_id: str) -> None: def edit_campaign(self, campaign_id: str) -> None:
camp = self.app.model.get_campaign(campaign_id) camp = self.app.model.get_campaign(campaign_id)
@ -128,9 +131,9 @@ class CampaignController:
# Campaign participant methods # Campaign participant methods
def add_campaign_participant(self) -> None: def create_campaign_participant(self) -> CampaignParticipant | None:
if not self.app.navigation.selected_campaign_id: if not self.app.navigation.selected_campaign_id:
return return None
participants = self.app.model.get_available_war_participants( participants = self.app.model.get_available_war_participants(
self.app.navigation.selected_campaign_id self.app.navigation.selected_campaign_id
) )
@ -140,17 +143,15 @@ class CampaignController:
] ]
dialog = CampaignParticipantDialog(self.app.view, participants=part_opts) dialog = CampaignParticipantDialog(self.app.view, participants=part_opts)
if dialog.exec() != QDialog.DialogCode.Accepted: if dialog.exec() != QDialog.DialogCode.Accepted:
return return None
player_id = dialog.get_player_id() player_id = dialog.get_player_id()
leader = dialog.get_participant_leader() leader = dialog.get_participant_leader()
theme = dialog.get_participant_theme() theme = dialog.get_participant_theme()
if not player_id: if not player_id:
return return None
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.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: def edit_campaign_participant(self, participant_id: str) -> None:
camp_part = self.app.model.get_campaign_participant(participant_id) camp_part = self.app.model.get_campaign_participant(participant_id)
@ -191,9 +192,9 @@ class CampaignController:
# allow same objectives in different fields? # allow same objectives in different fields?
return True return True
def add_sector(self) -> None: def create_sector(self) -> Sector | None:
if not self.app.navigation.selected_campaign_id: if not self.app.navigation.selected_campaign_id:
return return None
war = self.app.model.get_war_by_campaign( war = self.app.model.get_war_by_campaign(
self.app.navigation.selected_campaign_id self.app.navigation.selected_campaign_id
) )
@ -212,7 +213,7 @@ class CampaignController:
self.app.view, default_name="", rounds=rnd_objs, objectives=obj_dtos self.app.view, default_name="", rounds=rnd_objs, objectives=obj_dtos
) )
if dialog.exec() != QDialog.DialogCode.Accepted: if dialog.exec() != QDialog.DialogCode.Accepted:
return return None
name = dialog.get_sector_name() name = dialog.get_sector_name()
round_id = dialog.get_round_id() round_id = dialog.get_round_id()
major_id = dialog.get_major_id() major_id = dialog.get_major_id()
@ -223,8 +224,8 @@ class CampaignController:
if not self._validate_sector_inputs( if not self._validate_sector_inputs(
name, round_id, major_id, minor_id, influence_id name, round_id, major_id, minor_id, influence_id
): ):
return return None
self.app.model.add_sector( return self.app.model.add_sector(
self.app.navigation.selected_campaign_id, self.app.navigation.selected_campaign_id,
name, name,
round_id, round_id,
@ -234,8 +235,6 @@ class CampaignController:
mission, mission,
description, description,
) )
self.app.is_dirty = True
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
def edit_sector(self, sector_id: str) -> None: def edit_sector(self, sector_id: str) -> None:
sect = self.app.model.get_sector(sector_id) sect = self.app.model.get_sector(sector_id)

View file

@ -3,6 +3,7 @@ from typing import TYPE_CHECKING
from PyQt6.QtWidgets import QMessageBox, QDialog from PyQt6.QtWidgets import QMessageBox, QDialog
from warchron.constants import RefreshScope from warchron.constants import RefreshScope
from warchron.model.player import Player
if TYPE_CHECKING: if TYPE_CHECKING:
from warchron.controller.app_controller import AppController from warchron.controller.app_controller import AppController
@ -21,16 +22,15 @@ class PlayerController:
return False return False
return True return True
def add_player(self) -> None: def create_player(self) -> Player | None:
dialog = PlayerDialog(self.app.view) dialog = PlayerDialog(self.app.view)
result = dialog.exec() # modal blocking dialog result = dialog.exec()
if result == QDialog.DialogCode.Accepted: if result != QDialog.DialogCode.Accepted:
return None
name = dialog.get_player_name() name = dialog.get_player_name()
if not self._validate_player_inputs(name): if not self._validate_player_inputs(name):
return return None
self.app.model.add_player(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: def edit_player(self, player_id: str) -> None:
play = self.app.model.get_player(player_id) play = self.app.model.get_player(player_id)

View file

@ -3,6 +3,7 @@ from typing import List, TYPE_CHECKING
from PyQt6.QtWidgets import QDialog, QMessageBox from PyQt6.QtWidgets import QDialog, QMessageBox
from warchron.constants import ItemType, RefreshScope, Icons, IconName from warchron.constants import ItemType, RefreshScope, Icons, IconName
from warchron.model.round import Round
if TYPE_CHECKING: if TYPE_CHECKING:
from warchron.controller.app_controller import AppController 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.display_round_battles(battles_for_display)
self.app.view.endRoundBtn.setEnabled(not rnd.is_over) self.app.view.endRoundBtn.setEnabled(not rnd.is_over)
def add_round(self) -> None: def create_round(self) -> Round | None:
if not self.app.navigation.selected_campaign_id: campaign_id = self.app.navigation.selected_campaign_id
return if not campaign_id:
rnd = self.app.model.add_round(self.app.navigation.selected_campaign_id) return None
self.app.is_dirty = True return self.app.model.add_round(campaign_id)
self.app.navigation.refresh_and_select(
RefreshScope.WARS_TREE, item_type=ItemType.ROUND, item_id=rnd.id
)
def close_round(self) -> None: def close_round(self) -> None:
round_id = self.app.navigation.selected_round_id round_id = self.app.navigation.selected_round_id

View file

@ -2,7 +2,7 @@ from typing import List, TYPE_CHECKING
from PyQt6.QtWidgets import QMessageBox, QDialog from PyQt6.QtWidgets import QMessageBox, QDialog
from warchron.constants import ItemType, RefreshScope from warchron.constants import RefreshScope
if TYPE_CHECKING: if TYPE_CHECKING:
from warchron.controller.app_controller import AppController from warchron.controller.app_controller import AppController
@ -11,6 +11,9 @@ from warchron.controller.dtos import (
WarParticipantDTO, WarParticipantDTO,
ObjectiveDTO, 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.model.closure_service import ClosureService
from warchron.view.war_dialog import WarDialog from warchron.view.war_dialog import WarDialog
from warchron.view.objective_dialog import ObjectiveDialog from warchron.view.objective_dialog import ObjectiveDialog
@ -60,21 +63,18 @@ class WarController:
return False return False
return True return True
def add_war(self) -> None: def create_war(self) -> War | None:
dialog = WarDialog( dialog = WarDialog(
self.app.view, default_year=self.app.model.get_default_war_values()["year"] self.app.view, default_year=self.app.model.get_default_war_values()["year"]
) )
result = dialog.exec() # modal blocking dialog result = dialog.exec()
if result == QDialog.DialogCode.Accepted: if result != QDialog.DialogCode.Accepted:
return None
name = dialog.get_war_name() name = dialog.get_war_name()
year = dialog.get_war_year() year = dialog.get_war_year()
if not self._validate_war_inputs(name, year): if not self._validate_war_inputs(name, year):
return return None
war = self.app.model.add_war(name, year) return 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: def edit_war(self, war_id: str) -> None:
war = self.app.model.get_war(war_id) war = self.app.model.get_war(war_id)
@ -142,21 +142,19 @@ class WarController:
return False return False
return True return True
def add_objective(self) -> None: def create_objective(self) -> Objective | None:
if not self.app.navigation.selected_war_id: if not self.app.navigation.selected_war_id:
return return None
dialog = ObjectiveDialog(self.app.view) dialog = ObjectiveDialog(self.app.view)
if dialog.exec() != QDialog.DialogCode.Accepted: if dialog.exec() != QDialog.DialogCode.Accepted:
return return None
name = dialog.get_objective_name() name = dialog.get_objective_name()
description = dialog.get_objective_description() description = dialog.get_objective_description()
if not self._validate_objective_inputs(name, description): if not self._validate_objective_inputs(name, description):
return return None
self.app.model.add_objective( return self.app.model.add_objective(
self.app.navigation.selected_war_id, name, description 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: def edit_objective(self, objective_id: str) -> None:
obj = self.app.model.get_objective(objective_id) obj = self.app.model.get_objective(objective_id)
@ -174,9 +172,9 @@ class WarController:
# War participant methods # War participant methods
def add_war_participant(self) -> None: def create_war_participant(self) -> WarParticipant | None:
if not self.app.navigation.selected_war_id: if not self.app.navigation.selected_war_id:
return return None
players = self.app.model.get_available_players( players = self.app.model.get_available_players(
self.app.navigation.selected_war_id self.app.navigation.selected_war_id
) )
@ -185,16 +183,14 @@ class WarController:
] ]
dialog = WarParticipantDialog(self.app.view, players=play_opts) dialog = WarParticipantDialog(self.app.view, players=play_opts)
if dialog.exec() != QDialog.DialogCode.Accepted: if dialog.exec() != QDialog.DialogCode.Accepted:
return return None
player_id = dialog.get_player_id() player_id = dialog.get_player_id()
faction = dialog.get_participant_faction() faction = dialog.get_participant_faction()
if not player_id: if not player_id:
return return None
self.app.model.add_war_participant( return self.app.model.add_war_participant(
self.app.navigation.selected_war_id, player_id, faction 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: def edit_war_participant(self, participant_id: str) -> None:
war_part = self.app.model.get_war_participant(participant_id) war_part = self.app.model.get_war_participant(participant_id)

View file

@ -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

View file

@ -159,6 +159,8 @@ class Model:
def update_war(self, war_id: str, *, name: str, year: int) -> None: def update_war(self, war_id: str, *, name: str, year: int) -> None:
war = self.get_war(war_id) war = self.get_war(war_id)
if war.is_over:
raise ForbiddenOperation("Can't update a closed war.")
war.set_name(name) war.set_name(name)
war.set_year(year) war.set_year(year)

View file

@ -37,3 +37,9 @@ class ScoreService:
if sector.minor_objective_id: if sector.minor_objective_id:
totals[sector.minor_objective_id] += war.minor_value totals[sector.minor_objective_id] += war.minor_value
return totals return totals
# def compute_round_results(round)
# def compute_campaign_winner(campaign)
# def compute_war_winner(war)

View file

@ -238,6 +238,8 @@ class War:
if self.is_over: if self.is_over:
raise ForbiddenOperation("Can't update campaign in a closed war.") raise ForbiddenOperation("Can't update campaign in a closed war.")
camp = self.get_campaign(campaign_id) camp = self.get_campaign(campaign_id)
if camp.is_over:
raise ForbiddenOperation("Can't update a closed campaign.")
camp.set_name(name) camp.set_name(name)
camp.set_month(month) camp.set_month(month)

View file

@ -41,8 +41,7 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow):
self.majorValue.setMinimum(0) self.majorValue.setMinimum(0)
self.minorValue.setMinimum(0) self.minorValue.setMinimum(0)
self.on_influence_token_changed: Callable[[int], None] | None = None self.on_influence_token_changed: Callable[[int], None] | None = None
self.on_add_campaign: Callable[[], None] | None = None self.on_add_item: Callable[[str], None] | None = None
self.on_add_round: Callable[[], None] | None = None
self.on_edit_item: Callable[[str, 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.on_delete_item: Callable[[str, str], None] | None = None
self.splitter.setSizes([200, 800]) self.splitter.setSizes([200, 800])
@ -202,12 +201,12 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow):
# Wars view # Wars view
def _on_add_campaign_clicked(self) -> None: def _on_add_campaign_clicked(self) -> None:
if self.on_add_campaign: if self.on_add_item:
self.on_add_campaign() self.on_add_item(ItemType.CAMPAIGN)
def _on_add_round_clicked(self) -> None: def _on_add_round_clicked(self) -> None:
if self.on_add_round: if self.on_add_item:
self.on_add_round() self.on_add_item(ItemType.ROUND)
def set_add_campaign_enabled(self, enabled: bool) -> None: def set_add_campaign_enabled(self, enabled: bool) -> None:
self.addCampaignBtn.setEnabled(enabled) self.addCampaignBtn.setEnabled(enabled)