manage deleted refered player, participant, objective, sector

This commit is contained in:
Maxime Réaux 2026-02-05 16:17:18 +01:00
parent 7afbb5ea1d
commit df846b8e4b
6 changed files with 322 additions and 80 deletions

View file

@ -4,6 +4,11 @@ from pathlib import Path
from PyQt6.QtWidgets import QMessageBox, QDialog
from warchron.model.model import Model
from warchron.model.exception import (
DeletionForbidden,
DeletionRequiresConfirmation,
UpdateRequiresConfirmation,
)
from warchron.view.view import View
from warchron.constants import ItemType, RefreshScope
from warchron.controller.dtos import (
@ -362,38 +367,49 @@ class Controller:
self.view.select_tree_item(item_type=item_type, item_id=item_id)
def edit_item(self, item_type: str, item_id: str) -> None:
if item_type == ItemType.PLAYER:
self.edit_player(item_id)
self.refresh(RefreshScope.PLAYERS_LIST)
elif item_type == ItemType.WAR:
self.edit_war(item_id)
self.refresh_and_select(
RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=item_id
try:
if item_type == ItemType.PLAYER:
self.edit_player(item_id)
self.refresh(RefreshScope.PLAYERS_LIST)
elif item_type == ItemType.WAR:
self.edit_war(item_id)
self.refresh_and_select(
RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=item_id
)
elif item_type == ItemType.CAMPAIGN:
self.edit_campaign(item_id)
self.refresh_and_select(
RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=item_id
)
elif item_type == ItemType.OBJECTIVE:
self.edit_objective(item_id)
self.refresh(RefreshScope.WAR_DETAILS)
elif item_type == ItemType.WAR_PARTICIPANT:
self.edit_war_participant(item_id)
self.refresh(RefreshScope.WAR_DETAILS)
elif item_type == ItemType.SECTOR:
self.edit_sector(item_id)
self.refresh(RefreshScope.CAMPAIGN_DETAILS)
elif item_type == ItemType.CAMPAIGN_PARTICIPANT:
self.edit_campaign_participant(item_id)
self.refresh(RefreshScope.CAMPAIGN_DETAILS)
elif item_type == ItemType.CHOICE:
self.edit_round_choice(item_id)
self.refresh(RefreshScope.ROUND_DETAILS)
elif item_type == ItemType.BATTLE:
self.edit_round_battle(item_id)
self.refresh(RefreshScope.ROUND_DETAILS)
self.is_dirty = True
except UpdateRequiresConfirmation as e:
reply = QMessageBox.question(
self.view,
"Confirm update",
e.message,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
elif item_type == ItemType.CAMPAIGN:
self.edit_campaign(item_id)
self.refresh_and_select(
RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=item_id
)
elif item_type == ItemType.OBJECTIVE:
self.edit_objective(item_id)
self.refresh(RefreshScope.WAR_DETAILS)
elif item_type == ItemType.WAR_PARTICIPANT:
self.edit_war_participant(item_id)
self.refresh(RefreshScope.WAR_DETAILS)
elif item_type == ItemType.SECTOR:
self.edit_sector(item_id)
self.refresh(RefreshScope.CAMPAIGN_DETAILS)
elif item_type == ItemType.CAMPAIGN_PARTICIPANT:
self.edit_campaign_participant(item_id)
self.refresh(RefreshScope.CAMPAIGN_DETAILS)
elif item_type == ItemType.CHOICE:
self.edit_round_choice(item_id)
self.refresh(RefreshScope.ROUND_DETAILS)
elif item_type == ItemType.BATTLE:
self.edit_round_battle(item_id)
self.refresh(RefreshScope.ROUND_DETAILS)
self.is_dirty = True
if reply == QMessageBox.StandardButton.Yes:
e.apply_update()
self.refresh(RefreshScope.CAMPAIGN_DETAILS)
def delete_item(self, item_type: str, item_id: str) -> None:
reply = QMessageBox.question(
@ -404,39 +420,56 @@ class Controller:
)
if reply != QMessageBox.StandardButton.Yes:
return
if item_type == ItemType.PLAYER:
self.model.remove_player(item_id)
self.refresh(RefreshScope.PLAYERS_LIST)
elif item_type == ItemType.WAR:
self.model.remove_war(item_id)
self.refresh(RefreshScope.WARS_TREE)
elif item_type == ItemType.CAMPAIGN:
war = self.model.get_war_by_campaign(item_id)
war_id = war.id
self.model.remove_campaign(item_id)
self.refresh_and_select(
RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id
try:
if item_type == ItemType.PLAYER:
self.model.remove_player(item_id)
self.refresh(RefreshScope.PLAYERS_LIST)
elif item_type == ItemType.WAR:
self.model.remove_war(item_id)
self.refresh(RefreshScope.WARS_TREE)
elif item_type == ItemType.CAMPAIGN:
war = self.model.get_war_by_campaign(item_id)
war_id = war.id
self.model.remove_campaign(item_id)
self.refresh_and_select(
RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id
)
elif item_type == ItemType.OBJECTIVE:
self.model.remove_objective(item_id)
self.refresh(RefreshScope.WAR_DETAILS)
elif item_type == ItemType.WAR_PARTICIPANT:
self.model.remove_war_participant(item_id)
self.refresh(RefreshScope.WAR_DETAILS)
elif item_type == ItemType.SECTOR:
self.model.remove_sector(item_id)
self.refresh(RefreshScope.CAMPAIGN_DETAILS)
elif item_type == ItemType.CAMPAIGN_PARTICIPANT:
self.model.remove_campaign_participant(item_id)
self.refresh(RefreshScope.CAMPAIGN_DETAILS)
elif item_type == ItemType.ROUND:
camp = self.model.get_campaign_by_round(item_id)
camp_id = camp.id
self.model.remove_round(item_id)
self.refresh_and_select(
RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp_id
)
self.is_dirty = True
except DeletionForbidden as e:
QMessageBox.warning(
self.view,
"Deletion forbidden",
e.reason,
)
elif item_type == ItemType.OBJECTIVE:
self.model.remove_objective(item_id)
self.refresh(RefreshScope.WAR_DETAILS)
elif item_type == ItemType.WAR_PARTICIPANT:
self.model.remove_war_participant(item_id)
self.refresh(RefreshScope.WAR_DETAILS)
elif item_type == ItemType.SECTOR:
self.model.remove_sector(item_id)
self.refresh(RefreshScope.CAMPAIGN_DETAILS)
elif item_type == ItemType.CAMPAIGN_PARTICIPANT:
self.model.remove_campaign_participant(item_id)
self.refresh(RefreshScope.CAMPAIGN_DETAILS)
elif item_type == ItemType.ROUND:
camp = self.model.get_campaign_by_round(item_id)
camp_id = camp.id
self.model.remove_round(item_id)
self.refresh_and_select(
RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp_id
except DeletionRequiresConfirmation as e:
reply = QMessageBox.question(
self.view,
"Confirm deletion",
e.message,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
self.is_dirty = True
if reply == QMessageBox.StandardButton.Yes:
e.cleanup_action()
self.refresh(RefreshScope.CAMPAIGN_DETAILS)
# Player methods

View file

@ -2,6 +2,10 @@ from __future__ import annotations
from uuid import uuid4
from typing import Any, Dict, List
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
@ -94,12 +98,47 @@ class Campaign:
part.set_theme(theme)
def remove_campaign_participant(self, participant_id: str) -> None:
# TODO manage choices referring to it
# TODO manage battles referring to it
del self.participants[participant_id]
rounds_blocking: list[Round] = []
for rnd in self.rounds:
if rnd.has_choice_with_participant(
participant_id
) or rnd.has_battle_with_participant(participant_id):
rounds_blocking.append(rnd)
if not rounds_blocking:
del self.participants[participant_id]
return
def cleanup() -> None:
for rnd in rounds_blocking:
rnd.clear_participant_references(participant_id)
rnd.remove_choice(participant_id)
del self.participants[participant_id]
rounds_str = ", ".join(
str(self.get_round_index(rnd.id)) for rnd in rounds_blocking
)
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
def has_sector(self, sector_id: str) -> bool:
return sector_id in self.sectors
def has_sector_with_objective(self, objective_id: str) -> bool:
return any(
sect.major_objective_id == objective_id
or sect.minor_objective_id == objective_id
or sect.influence_objective_id == objective_id
for sect in self.sectors.values()
)
def add_sector(
self, name: str, round_id: str, major_id: str, minor_id: str, influence_id: str
) -> Sector:
@ -130,16 +169,75 @@ class Campaign:
influence_id: str,
) -> None:
sect = self.get_sector(sector_id)
sect.set_name(name)
sect.set_round(round_id)
sect.set_major(major_id)
sect.set_minor(minor_id)
sect.set_influence(influence_id)
old_round_id = sect.round_id
def apply_update() -> None:
sect.set_name(name)
sect.set_round(round_id)
sect.set_major(major_id)
sect.set_minor(minor_id)
sect.set_influence(influence_id)
if old_round_id == round_id:
apply_update()
return
affected_rounds: list[Round] = []
for rnd in self.rounds:
if rnd.id == old_round_id and (
rnd.has_choice_with_sector(sector_id)
or rnd.has_battle_with_sector(sector_id)
):
affected_rounds.append(rnd)
if not affected_rounds:
apply_update()
return
def cleanup_and_update() -> None:
for rnd in affected_rounds:
rnd.clear_sector_references(sector_id)
rnd.remove_battle(sector_id)
apply_update()
rounds_str = ", ".join(
str(self.get_round_index(rnd.id)) for rnd in affected_rounds
)
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:
# TODO manage choices referring to it
# TODO manage battles referring to it
del self.sectors[sector_id]
rounds_blocking: list[Round] = []
for rnd in self.rounds:
if rnd.has_battle_with_sector(sector_id) or rnd.has_choice_with_sector(
sector_id
):
rounds_blocking.append(rnd)
if not rounds_blocking:
del self.sectors[sector_id]
return
def cleanup() -> None:
for rnd in rounds_blocking:
rnd.clear_sector_references(sector_id)
rnd.remove_battle(sector_id)
del self.sectors[sector_id]
rounds_str = ", ".join(
str(self.get_round_index(rnd.id)) for rnd in rounds_blocking
)
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]:
sectors = [s for s in self.sectors.values() if s.round_id == round_id]

View file

@ -0,0 +1,31 @@
from typing import Callable
class DeletionForbidden(Exception):
def __init__(self, reason: str):
self.reason = reason
super().__init__(reason)
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)

View file

@ -4,6 +4,7 @@ import json
import shutil
from datetime import datetime
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
@ -84,7 +85,16 @@ class Model:
return list(self.players.values())
def remove_player(self, player_id: str) -> None:
# TODO manage war_participants referring to it
wars_using_player: List[str] = []
for war in self.wars.values():
if war.has_player(player_id):
wars_using_player.append(war.name)
if wars_using_player:
wars_str = ", ".join(wars_using_player)
raise DeletionForbidden(
f"This player is participating in war(s): {wars_str}.\n"
"Remove it from participants first."
)
del self.players[player_id]
# War methods
@ -100,6 +110,7 @@ class Model:
def get_war(self, id: str) -> War:
return self.wars[id]
# TODO replace multiloops by internal has_* method
def get_war_by_campaign(self, campaign_id: str) -> War:
for war in self.wars.values():
for camp in war.campaigns:
@ -107,6 +118,7 @@ class Model:
return war
raise KeyError(f"Campaign {campaign_id} not found in any War")
# TODO replace multiloops by internal has_* method
def get_war_by_sector(self, sector_id: str) -> War:
for war in self.wars.values():
for camp in war.campaigns:
@ -115,6 +127,7 @@ class Model:
return war
raise KeyError(f"Sector {sector_id} not found in any War")
# TODO replace multiloops by internal has_* method
def get_war_by_round(self, round_id: str) -> War:
for war in self.wars.values():
for camp in war.campaigns:
@ -123,6 +136,7 @@ class Model:
return war
raise KeyError(f"Round {round_id} not found in any War")
# TODO replace multiloops by internal has_* method
def get_war_by_objective(self, objective_id: str) -> War:
for war in self.wars.values():
for obj in war.objectives.values():
@ -160,6 +174,7 @@ class Model:
war = self.get_war(war_id)
return war.add_objective(name, description)
# TODO replace multiloops by internal has_* method
def get_objective(self, objective_id: str) -> Objective:
for war in self.wars.values():
for obj in war.objectives.values():
@ -191,6 +206,7 @@ class Model:
war = self.get_war(war_id)
return war.add_war_participant(player_id, faction)
# TODO replace multiloops by internal has_* method
def get_war_participant(self, participant_id: str) -> WarParticipant:
for war in self.wars.values():
for part in war.participants.values():
@ -219,6 +235,7 @@ class Model:
war = self.get_war(war_id)
return war.add_campaign(name, month)
# TODO replace multiloops by internal has_* method
def get_campaign(self, campaign_id: str) -> Campaign:
for war in self.wars.values():
for campaign in war.campaigns:
@ -269,6 +286,7 @@ class Model:
camp = self.get_campaign(campaign_id)
return camp.add_sector(name, round_id, major_id, minor_id, influence_id)
# TODO replace multiloops by internal has_* method
def get_sector(self, sector_id: str) -> Sector:
for war in self.wars.values():
for camp in war.campaigns:
@ -318,6 +336,7 @@ class Model:
war_part = war.get_war_participant(participant_id)
return self.players[war_part.player_id].name
# TODO replace multiloops by internal has_* method
def get_campaign_participant(self, participant_id: str) -> CampaignParticipant:
for war in self.wars.values():
for camp in war.campaigns:
@ -352,6 +371,7 @@ class Model:
war = self.get_war_by_campaign(campaign_id)
return war.add_round(campaign_id)
# TODO replace multiloops by internal has_* method
def get_round(self, round_id: str) -> Round:
for war in self.wars.values():
for camp in war.campaigns:

View file

@ -43,6 +43,18 @@ class Round:
def get_choice(self, participant_id: str) -> Choice | None:
return self.choices.get(participant_id)
def has_choice_with_sector(self, sector_id: str) -> bool:
return any(
choice.priority_sector_id == sector_id
or choice.secondary_sector_id == sector_id
for choice in self.choices.values()
)
def has_choice_with_participant(self, participant_id: str) -> bool:
return any(
choice.participant_id == participant_id for choice in self.choices.values()
)
def create_choice(self, participant_id: str) -> Choice:
if participant_id not in self.choices:
choice = Choice(
@ -66,6 +78,13 @@ class Round:
choice.set_secondary(secondary_sector_id)
choice.set_comment(comment)
def clear_sector_references(self, sector_id: str) -> None:
for choice in self.choices.values():
if choice.priority_sector_id == sector_id:
choice.priority_sector_id = None
if choice.secondary_sector_id == sector_id:
choice.secondary_sector_id = None
def remove_choice(self, participant_id: str) -> None:
del self.choices[participant_id]
@ -74,6 +93,15 @@ class Round:
def get_battle(self, sector_id: str) -> Battle | None:
return self.battles.get(sector_id)
def has_battle_with_sector(self, sector_id: str) -> bool:
return any(bat.sector_id == sector_id for bat in self.battles.values())
def has_battle_with_participant(self, participant_id: str) -> bool:
return any(
bat.player_1_id == participant_id or bat.player_2_id == participant_id
for bat in self.battles.values()
)
def create_battle(self, sector_id: str) -> Battle:
if sector_id not in self.battles:
battle = Battle(sector_id=sector_id, player_1_id=None, player_2_id=None)
@ -99,5 +127,14 @@ class Round:
bat.set_victory_condition(victory_condition)
bat.set_comment(comment)
def clear_participant_references(self, participant_id: str) -> None:
for battle in self.battles.values():
if battle.player_1_id == participant_id:
battle.player_1_id = None
if battle.player_2_id == participant_id:
battle.player_2_id = None
if battle.winner_id == participant_id:
battle.winner_id = None
def remove_battle(self, sector_id: str) -> None:
del self.battles[sector_id]

View file

@ -3,6 +3,7 @@ from uuid import uuid4
from datetime import datetime
from typing import Any, Dict, List
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
@ -82,7 +83,16 @@ class War:
obj.set_description(description)
def remove_objective(self, objective_id: str) -> None:
# TODO manage sectors referring to it
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 DeletionForbidden(
f"This objective is used in campaign(s) sector(s): {camps_str}.\n"
"Remove it from campaign(s) first."
)
del self.objectives[objective_id]
# War participant methods
@ -109,14 +119,23 @@ class War:
def get_all_war_participants(self) -> List[WarParticipant]:
return list(self.participants.values())
def update_war_participant(self, player_id: str, *, faction: str) -> None:
part = self.get_war_participant(player_id)
def update_war_participant(self, participant_id: str, *, faction: str) -> None:
part = self.get_war_participant(participant_id)
# Can't change referred Model.players
part.set_faction(faction)
def remove_war_participant(self, player_id: str) -> None:
# TODO manage campaign_participants referring to it
del self.participants[player_id]
def remove_war_participant(self, participant_id: str) -> None:
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 DeletionForbidden(
f"This war participant is used in campaign(s): {camps_str}.\n"
"Remove it from campaign(s) first."
)
del self.participants[participant_id]
# Campaign methods
@ -139,6 +158,7 @@ class War:
return camp
raise KeyError(f"Campaign {campaign_id} not found in War {self.id}")
# TODO replace multiloops by internal has_* method
def get_campaign_by_round(self, round_id: str) -> Campaign | None:
for camp in self.campaigns:
for rnd in camp.rounds:
@ -146,6 +166,7 @@ class War:
return camp
return None
# TODO replace multiloops by internal has_* method
def get_campaign_by_sector(self, sector_id: str) -> Campaign:
for camp in self.campaigns:
for sect in camp.sectors.values():
@ -185,6 +206,7 @@ class War:
camp = self.get_campaign(campaign_id)
return camp.add_sector(name, round_id, major_id, minor_id, influence_id)
# TODO replace multiloops by internal has_* method
def get_sector(self, sector_id: str) -> Sector:
for camp in self.campaigns:
for sect in camp.sectors.values():
@ -232,6 +254,7 @@ class War:
camp = self.get_campaign(campaign_id)
return camp.add_campaign_participant(participant_id, leader, theme)
# TODO replace multiloops by internal has_* method
def get_campaign_participant(self, participant_id: str) -> CampaignParticipant:
for camp in self.campaigns:
for part in camp.participants.values():