code quality (mypy+flake8)

This commit is contained in:
Maxime Réaux 2026-02-04 16:10:53 +01:00
parent 7210ddc927
commit 55abdccc64
19 changed files with 778 additions and 497 deletions

View file

@ -1,11 +1,24 @@
from typing import List
from pathlib import Path
from PyQt6.QtWidgets import QMessageBox, QDialog
from warchron.model.model import Model
from warchron.view.view import View
from warchron.constants import ItemType, RefreshScope
from warchron.controller.dtos import ParticipantOption
from warchron.controller.dtos import (
ParticipantOption,
TreeSelection,
WarDTO,
WarParticipantDTO,
ObjectiveDTO,
CampaignDTO,
CampaignParticipantDTO,
SectorDTO,
RoundDTO,
ChoiceDTO,
BattleDTO,
)
from warchron.view.view import (
PlayerDialog,
WarDialog,
@ -20,13 +33,13 @@ from warchron.view.view import (
class Controller:
def __init__(self, model: Model, view: View):
def __init__(self, model: Model, view: View) -> None:
self.model: Model = model
self.view: View = view
self.current_file: Path | None = None
self.selected_war_id: str = None
self.selected_campaign_id: str = None
self.selected_round_id: str = None
self.selected_war_id: str | None = None
self.selected_campaign_id: str | None = None
self.selected_round_id: str | None = None
self.view.on_close_callback = self.on_app_close
self.is_dirty: bool = False
self.__connect()
@ -38,7 +51,7 @@ class Controller:
self.view.on_add_campaign = self.add_campaign
self.view.on_add_round = self.add_round
def __connect(self):
def __connect(self) -> None:
self.view.actionExit.triggered.connect(self.view.close)
self.view.actionNew.triggered.connect(self.new)
self.view.actionOpen.triggered.connect(self.open_file)
@ -73,7 +86,7 @@ class Controller:
# Menu bar methods
def new(self):
def new(self) -> None:
if self.is_dirty:
reply = QMessageBox.question(
self.view,
@ -90,7 +103,7 @@ class Controller:
self.refresh_wars_view()
self.update_window_title()
def open_file(self):
def open_file(self) -> None:
if self.is_dirty:
reply = QMessageBox.question(
self.view,
@ -110,7 +123,7 @@ class Controller:
self.refresh_wars_view()
self.update_window_title()
def save(self):
def save(self) -> None:
if not self.current_file:
self.save_as()
return
@ -118,7 +131,7 @@ class Controller:
self.is_dirty = False
self.update_window_title()
def save_as(self):
def save_as(self) -> None:
path = self.view.ask_save_file()
if not path:
return
@ -129,83 +142,102 @@ class Controller:
# Display methods
def update_window_title(self):
def update_window_title(self) -> None:
base = "WarChron"
if self.current_file:
base += f" - {self.current_file.name}"
else:
base += f" - New file"
base += " - New file"
if self.is_dirty:
base = base + " *"
self.view.setWindowTitle(base)
def refresh_players_view(self):
def refresh_players_view(self) -> None:
players = self.model.get_all_players()
self.view.display_players(players)
players_for_display: List[ParticipantOption] = [
ParticipantOption(id=p.id, name=p.name) for p in players
]
self.view.display_players(players_for_display)
def refresh_wars_view(self):
wars = self.model.get_all_wars()
def refresh_wars_view(self) -> None:
wars: List[WarDTO] = [
WarDTO(
id=w.id,
name=w.name,
year=w.year,
_campaigns=[
CampaignDTO(
id=c.id,
name=c.name,
month=c.month,
_rounds=[
RoundDTO(id=r.id, index=c.get_round_index(r.id))
for r in c.get_all_rounds()
],
)
for c in w.get_all_campaigns()
],
)
for w in self.model.get_all_wars()
]
self.view.display_wars_tree(wars)
def _fill_war_details(self, war_id: str):
def _fill_war_details(self, war_id: str) -> None:
war = self.model.get_war(war_id)
self.view.show_war_details(name=war.name, year=war.year)
objectives = war.get_all_objectives()
self.view.display_war_objectives(objectives)
objectives_for_display: List[ObjectiveDTO] = [
ObjectiveDTO(id=obj.id, name=obj.name, description=obj.description)
for obj in objectives
]
self.view.display_war_objectives(objectives_for_display)
war_parts = war.get_all_war_participants()
participants_for_display = [
(
self.model.get_player_name(
p.player_id,
),
p.faction,
p.id,
participants_for_display: List[WarParticipantDTO] = [
WarParticipantDTO(
id=p.id,
player_name=self.model.get_player_name(p.player_id),
faction=p.faction,
)
for p in war_parts
]
self.view.display_war_participants(participants_for_display)
def _fill_campaign_details(self, campaign_id: str):
def _fill_campaign_details(self, campaign_id: str) -> None:
camp = self.model.get_campaign(campaign_id)
self.view.show_campaign_details(name=camp.name, month=camp.month)
sectors = camp.get_all_sectors()
sectors_for_display = []
war = self.model.get_war_by_campaign(camp.id)
for sect in sectors:
round_index = camp.get_round_index(sect.round_id)
major_name = war.get_objective_name(sect.major_objective_id)
minor_name = war.get_objective_name(sect.minor_objective_id)
influence_name = war.get_objective_name(sect.influence_objective_id)
sectors_for_display.append(
(
sect.name,
round_index,
major_name,
minor_name,
influence_name,
sect.id,
)
sectors_for_display: List[SectorDTO] = [
SectorDTO(
id=sect.id,
name=sect.name,
round_index=camp.get_round_index(sect.round_id),
major=war.get_objective_name(sect.major_objective_id),
minor=war.get_objective_name(sect.minor_objective_id),
influence=war.get_objective_name(sect.influence_objective_id),
)
for sect in sectors
]
self.view.display_campaign_sectors(sectors_for_display)
camp_parts = camp.get_all_campaign_participants()
participants_for_display = [
(
self.model.get_participant_name(p.war_participant_id),
p.leader,
p.theme,
p.id,
participants_for_display: List[CampaignParticipantDTO] = [
CampaignParticipantDTO(
id=p.id,
player_name=self.model.get_participant_name(p.war_participant_id),
leader=p.leader or "",
theme=p.theme or "",
)
for p in camp_parts
]
self.view.display_campaign_participants(participants_for_display)
def _fill_round_details(self, round_id: str):
def _fill_round_details(self, round_id: str) -> None:
rnd = self.model.get_round(round_id)
camp = self.model.get_campaign_by_round(round_id)
self.view.show_round_details(index=camp.get_round_index(round_id))
participants = self.model.get_round_participants(round_id)
sectors = camp.get_sectors_in_round(round_id)
choices_for_display = []
choices_for_display: List[ChoiceDTO] = []
for part in participants:
choice = rnd.get_choice(part.id)
if not choice:
@ -213,21 +245,28 @@ class Controller:
round_id=rnd.id, participant_id=part.id
)
priority_name = (
camp.get_sector_name(choice.priority_sector_id) if choice else ""
camp.get_sector_name(choice.priority_sector_id)
if choice.priority_sector_id is not None
else ""
)
secondary_name = (
camp.get_sector_name(choice.secondary_sector_id) if choice else ""
camp.get_sector_name(choice.secondary_sector_id)
if choice.secondary_sector_id is not None
else ""
)
choices_for_display.append(
(
self.model.get_participant_name(part.war_participant_id),
priority_name,
secondary_name,
choice.participant_id,
ChoiceDTO(
id=choice.participant_id,
participant_name=self.model.get_participant_name(
part.war_participant_id
),
priority_sector=priority_name,
secondary_sector=secondary_name,
comment=choice.comment,
)
)
self.view.display_round_choices(choices_for_display)
battles_for_display = []
battles_for_display: List[BattleDTO] = []
for sect in sectors:
battle = rnd.get_battle(sect.id)
if not battle:
@ -246,23 +285,34 @@ class Controller:
)
else:
player_2_name = ""
if battle.winner_id:
camp_part = camp.participants[battle.winner_id]
winner_name = self.model.get_participant_name(
camp_part.war_participant_id
)
else:
winner_name = ""
battles_for_display.append(
(
camp.get_sector_name(battle.sector_id),
player_1_name,
player_2_name,
battle.sector_id,
BattleDTO(
id=battle.sector_id,
sector_name=camp.get_sector_name(battle.sector_id),
player_1=player_1_name,
player_2=player_2_name,
winner=winner_name,
score=battle.score,
victory_condition=battle.victory_condition,
comment=battle.comment,
)
)
self.view.display_round_battles(battles_for_display)
def on_tree_selection_changed(self, selection):
def on_tree_selection_changed(self, selection: TreeSelection | None) -> None:
self.selected_war_id = None
self.selected_campaign_id = None
self.selected_round_id = None
if selection:
item_type = selection["type"]
item_id = selection["id"]
item_type = selection.type
item_id = selection.id
if item_type == ItemType.WAR:
self.selected_war_id = item_id
self.view.show_details(ItemType.WAR)
@ -281,11 +331,11 @@ class Controller:
return
self.update_actions_state()
def update_actions_state(self):
def update_actions_state(self) -> None:
self.view.set_add_campaign_enabled(self.selected_war_id is not None)
self.view.set_add_round_enabled(self.selected_campaign_id is not None)
def refresh(self, scope: RefreshScope):
def refresh(self, scope: RefreshScope) -> None:
match scope:
case RefreshScope.PLAYERS_LIST:
self.refresh_players_view()
@ -309,26 +359,28 @@ class Controller:
def refresh_and_select(
self, scope: RefreshScope, *, item_type: ItemType, item_id: str
):
) -> None:
self.refresh(scope)
self.view.select_tree_item(item_type=item_type, item_id=item_id)
def edit_item(self, item_type: str, item_id: str):
def edit_item(self, item_type: str, item_id: str) -> None:
if item_type == ItemType.PLAYER:
play = self.model.get_player(item_id)
dialog = PlayerDialog(self.view, default_name=play.name)
if dialog.exec() == QDialog.DialogCode.Accepted:
name = dialog.get_player_name()
player_dialog = PlayerDialog(self.view, default_name=play.name)
if player_dialog.exec() == QDialog.DialogCode.Accepted:
name = player_dialog.get_player_name()
if not self._validate_player_inputs(name):
return
self.model.update_player(item_id, name=name)
self.refresh(RefreshScope.PLAYERS_LIST)
elif item_type == ItemType.WAR:
war = self.model.get_war(item_id)
dialog = WarDialog(self.view, default_name=war.name, default_year=war.year)
if dialog.exec() == QDialog.DialogCode.Accepted:
name = dialog.get_war_name()
year = dialog.get_war_year()
war_dialog = WarDialog(
self.view, default_name=war.name, default_year=war.year
)
if war_dialog.exec() == QDialog.DialogCode.Accepted:
name = war_dialog.get_war_name()
year = war_dialog.get_war_year()
if not self._validate_war_inputs(name, year):
return
self.model.update_war(item_id, name=name, year=year)
@ -337,12 +389,12 @@ class Controller:
)
elif item_type == ItemType.CAMPAIGN:
camp = self.model.get_campaign(item_id)
dialog = CampaignDialog(
camp_dialog = CampaignDialog(
self.view, default_name=camp.name, default_month=camp.month
)
if dialog.exec() == QDialog.DialogCode.Accepted:
name = dialog.get_campaign_name()
month = dialog.get_campaign_month()
if camp_dialog.exec() == QDialog.DialogCode.Accepted:
name = camp_dialog.get_campaign_name()
month = camp_dialog.get_campaign_month()
if not self._validate_campaign_inputs(name, month):
return
self.model.update_campaign(item_id, name=name, month=month)
@ -351,28 +403,29 @@ class Controller:
)
elif item_type == ItemType.OBJECTIVE:
obj = self.model.get_objective(item_id)
dialog = ObjectiveDialog(
obj_dialog = ObjectiveDialog(
self.view, default_name=obj.name, default_description=obj.description
)
if dialog.exec() == QDialog.DialogCode.Accepted:
name = dialog.get_objective_name()
description = dialog.get_objective_description()
if obj_dialog.exec() == QDialog.DialogCode.Accepted:
name = obj_dialog.get_objective_name()
description = obj_dialog.get_objective_description()
if not self._validate_objective_inputs(name, description):
return
self.model.update_objective(item_id, name=name, description=description)
self.refresh(RefreshScope.WAR_DETAILS)
elif item_type == ItemType.WAR_PARTICIPANT:
camp_part = self.model.get_war_participant(item_id)
player = self.model.get_player(camp_part.player_id)
dialog = WarParticipantDialog(
war_part = self.model.get_war_participant(item_id)
player = self.model.get_player(war_part.player_id)
play_opt = ParticipantOption(id=player.id, name=player.name)
war_part_dialog = WarParticipantDialog(
self.view,
players=[player],
default_player_id=camp_part.id,
default_faction=camp_part.faction,
players=[play_opt],
default_player_id=war_part.id,
default_faction=war_part.faction,
editable_player=False,
)
if dialog.exec() == QDialog.DialogCode.Accepted:
faction = dialog.get_participant_faction()
if war_part_dialog.exec() == QDialog.DialogCode.Accepted:
faction = war_part_dialog.get_participant_faction()
self.model.update_war_participant(item_id, faction=faction)
self.refresh(RefreshScope.WAR_DETAILS)
elif item_type == ItemType.SECTOR:
@ -380,23 +433,30 @@ class Controller:
camp = self.model.get_campaign_by_sector(item_id)
war = self.model.get_war_by_campaign(camp.id)
rounds = camp.get_all_rounds()
rnd_dto: List[RoundDTO] = [
RoundDTO(id=rnd.id, index=i) for i, rnd in enumerate(rounds, start=1)
]
objectives = war.get_all_objectives()
dialog = SectorDialog(
obj_dto: List[ObjectiveDTO] = [
ObjectiveDTO(id=obj.id, name=obj.name, description=obj.description)
for obj in objectives
]
sect_dialog = SectorDialog(
self.view,
default_name=sect.name,
rounds=rounds,
rounds=rnd_dto,
default_round_id=sect.round_id,
objectives=objectives,
objectives=obj_dto,
default_major_id=sect.major_objective_id,
default_minor_id=sect.minor_objective_id,
default_influence_id=sect.influence_objective_id,
)
if dialog.exec() == QDialog.DialogCode.Accepted:
name = dialog.get_sector_name()
round_id = dialog.get_round_id()
major_id = dialog.get_major_id()
minor_id = dialog.get_minor_id()
influence_id = dialog.get_influence_id()
if sect_dialog.exec() == QDialog.DialogCode.Accepted:
name = sect_dialog.get_sector_name()
round_id = sect_dialog.get_round_id()
major_id = sect_dialog.get_major_id()
minor_id = sect_dialog.get_minor_id()
influence_id = sect_dialog.get_influence_id()
self.model.update_sector(
item_id,
name=name,
@ -411,7 +471,7 @@ class Controller:
war_part = self.model.get_war_participant(camp_part.war_participant_id)
player = self.model.get_player(war_part.player_id)
part_opt = [ParticipantOption(id=player.id, name=player.name)]
dialog = CampaignParticipantDialog(
camp_part_dialog = CampaignParticipantDialog(
self.view,
participants=part_opt,
default_participant_id=camp_part.id,
@ -419,9 +479,9 @@ class Controller:
default_theme=camp_part.theme,
editable_player=False,
)
if dialog.exec() == QDialog.DialogCode.Accepted:
leader = dialog.get_participant_leader()
theme = dialog.get_participant_theme()
if camp_part_dialog.exec() == QDialog.DialogCode.Accepted:
leader = camp_part_dialog.get_participant_leader()
theme = camp_part_dialog.get_participant_theme()
self.model.update_campaign_participant(
item_id, leader=leader, theme=theme
)
@ -433,7 +493,7 @@ class Controller:
self.edit_round_battle(item_id)
self.refresh(RefreshScope.ROUND_DETAILS)
def delete_item(self, item_type: str, item_id: str):
def delete_item(self, item_type: str, item_id: str) -> None:
reply = QMessageBox.question(
self.view,
"Confirm deletion",
@ -486,7 +546,7 @@ class Controller:
return False
return True
def add_player(self):
def add_player(self) -> None:
dialog = PlayerDialog(self.view)
result = dialog.exec() # modal blocking dialog
if result == QDialog.DialogCode.Accepted:
@ -510,7 +570,7 @@ class Controller:
return False
return True
def add_war(self):
def add_war(self) -> None:
dialog = WarDialog(
self.view, default_year=self.model.get_default_war_values()["year"]
)
@ -536,7 +596,7 @@ class Controller:
return False
return True
def add_objective(self):
def add_objective(self) -> None:
if not self.selected_war_id:
return
dialog = ObjectiveDialog(self.view)
@ -552,11 +612,14 @@ class Controller:
# War participant methods
def add_war_participant(self):
def add_war_participant(self) -> None:
if not self.selected_war_id:
return
players = self.model.get_available_players(self.selected_war_id)
dialog = WarParticipantDialog(self.view, players=players)
play_opts: List[ParticipantOption] = [
ParticipantOption(id=p.id, name=p.name) for p in players
]
dialog = WarParticipantDialog(self.view, players=play_opts)
if dialog.exec() != QDialog.DialogCode.Accepted:
return
player_id = dialog.get_player_id()
@ -582,7 +645,7 @@ class Controller:
return False
return True
def add_campaign(self):
def add_campaign(self) -> None:
if not self.selected_war_id:
return
dialog = CampaignDialog(
@ -605,7 +668,7 @@ class Controller:
# Campaign participant methods
def add_campaign_participant(self):
def add_campaign_participant(self) -> None:
if not self.selected_campaign_id:
return
participants = self.model.get_available_war_participants(
@ -634,6 +697,7 @@ class Controller:
def _validate_sector_inputs(
self, name: str, round_id: str, major_id: str, minor_id: str, influence_id: str
) -> bool:
if not name.strip():
QMessageBox.warning(
self.view, "Invalid name", "Sector name cannot be empty."
@ -642,15 +706,22 @@ class Controller:
# allow same objectives in different fields?
return True
def add_sector(self):
def add_sector(self) -> None:
if not self.selected_campaign_id:
return
war = self.model.get_war_by_campaign(self.selected_campaign_id)
camp = self.model.get_campaign(self.selected_campaign_id)
rounds = camp.get_all_rounds()
rnd_objs: List[RoundDTO] = [
RoundDTO(id=rnd.id, index=camp.get_round_index(rnd.id)) for rnd in rounds
]
objectives = war.get_all_objectives()
obj_dtos: List[ObjectiveDTO] = [
ObjectiveDTO(id=obj.id, name=obj.name, description=obj.description)
for obj in objectives
]
dialog = SectorDialog(
self.view, default_name="", rounds=rounds, objectives=objectives
self.view, default_name="", rounds=rnd_objs, objectives=obj_dtos
)
if dialog.exec() != QDialog.DialogCode.Accepted:
return
@ -671,7 +742,7 @@ class Controller:
# Round methods
def add_round(self):
def add_round(self) -> None:
if not self.selected_campaign_id:
return
rnd = self.model.add_round(self.selected_campaign_id)
@ -682,14 +753,25 @@ class Controller:
# Choice methods
def edit_round_choice(self, choice_id: str):
def edit_round_choice(self, choice_id: str) -> None:
round_id = self.selected_round_id
if not round_id:
return
# camp, rnd, participants, sectors = self.model.get_round_choices_data(round_id)
war = self.model.get_war_by_round(round_id)
camp = self.model.get_campaign_by_round(round_id)
rnd = camp.get_round(round_id)
sectors = camp.get_sectors_in_round(round_id)
sect_opts: List[SectorDTO] = [
SectorDTO(
id=sect.id,
name=sect.name,
round_index=camp.get_round_index(sect.round_id),
major=war.get_objective_name(sect.major_objective_id),
minor=war.get_objective_name(sect.minor_objective_id),
influence=war.get_objective_name(sect.influence_objective_id),
)
for sect in sectors
]
choice = rnd.get_choice(choice_id)
if not choice:
return
@ -700,7 +782,7 @@ class Controller:
self.view,
participants=[part_opt],
default_participant_id=participant.id,
sectors=sectors,
sectors=sect_opts,
default_priority_id=choice.priority_sector_id,
default_secondary_id=choice.secondary_sector_id,
default_comment=choice.comment,
@ -717,10 +799,11 @@ class Controller:
# Battle methods
def edit_round_battle(self, battle_id: str):
def edit_round_battle(self, battle_id: str) -> None:
round_id = self.selected_round_id
if not round_id:
return
war = self.model.get_war_by_round(round_id)
camp = self.model.get_campaign_by_round(round_id)
rnd = camp.get_round(round_id)
participants = camp.get_all_campaign_participants()
@ -728,13 +811,22 @@ class Controller:
if not battle:
return
sect = camp.sectors[battle.sector_id]
part_opts: list[ParticipantOption] = []
sect_dto = SectorDTO(
id=sect.id,
name=sect.name,
round_index=camp.get_round_index(sect.round_id),
major=war.get_objective_name(sect.major_objective_id),
minor=war.get_objective_name(sect.minor_objective_id),
influence=war.get_objective_name(sect.influence_objective_id),
)
part_opts: List[ParticipantOption] = []
for participant in participants:
player = self.model.get_player_from_campaign_participant(participant)
part_opts.append(ParticipantOption(id=participant.id, name=player.name))
dialog = BattlesDialog(
self.view,
sectors=[sect],
sectors=[sect_dto],
default_sector_id=sect.id,
players=part_opts,
default_player_1_id=battle.player_1_id,

View file

@ -1,3 +1,4 @@
from typing import List
from dataclasses import dataclass
@ -5,3 +6,90 @@ from dataclasses import dataclass
class ParticipantOption:
id: str
name: str
@dataclass(frozen=True, slots=True)
class TreeSelection:
type: str
id: str
@dataclass
class WarDTO:
id: str
name: str
year: int
_campaigns: List["CampaignDTO"] | None = None
def get_all_campaigns(self) -> List["CampaignDTO"]:
return self._campaigns or []
@dataclass(frozen=True, slots=True)
class ObjectiveDTO:
id: str
name: str
description: str
@dataclass(frozen=True, slots=True)
class WarParticipantDTO:
id: str
player_name: str
faction: str
@dataclass
class CampaignDTO:
id: str
name: str
month: int
_rounds: List["RoundDTO"] | None = None
def get_all_rounds(self) -> List["RoundDTO"]:
return self._rounds or []
@dataclass(frozen=True, slots=True)
class CampaignParticipantDTO:
id: str
player_name: str
leader: str
theme: str
@dataclass(frozen=True, slots=True)
class SectorDTO:
id: str
name: str
round_index: int
major: str
minor: str
influence: str
@dataclass
class RoundDTO:
id: str
index: int
@dataclass(frozen=True, slots=True)
class ChoiceDTO:
id: str
participant_name: str
priority_sector: str
secondary_sector: str
comment: str | None
@dataclass(frozen=True, slots=True)
class BattleDTO:
id: str
sector_name: str
player_1: str
player_2: str
winner: str | None
score: str | None
victory_condition: str | None
comment: str | None