From 185733b5d4dac0377028f4191e8fa0534841f875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20R=C3=A9aux?= Date: Thu, 22 Jan 2026 23:42:47 +0100 Subject: [PATCH] edit/delete player/war/campaign/round --- src/warchron/controller/controller.py | 133 ++++++++++++++++++--- src/warchron/model/campaign.py | 19 ++- src/warchron/model/model.py | 63 ++++++++-- src/warchron/model/war.py | 42 +++++-- src/warchron/view/ui/ui_campaign_dialog.py | 9 ++ src/warchron/view/ui/ui_campaign_dialog.ui | 29 +++++ src/warchron/view/ui/ui_war_dialog.py | 13 +- src/warchron/view/ui/ui_war_dialog.ui | 33 ++++- src/warchron/view/view.py | 80 +++++++++++-- 9 files changed, 363 insertions(+), 58 deletions(-) diff --git a/src/warchron/controller/controller.py b/src/warchron/controller/controller.py index 15a9461..62c1397 100644 --- a/src/warchron/controller/controller.py +++ b/src/warchron/controller/controller.py @@ -1,4 +1,5 @@ from pathlib import Path +from datetime import datetime from PyQt6.QtWidgets import QMessageBox, QDialog from warchron.model.model import Model @@ -33,6 +34,8 @@ class Controller: self.view.actionSave_as.triggered.connect(self.save_as) self.view.addPlayerBtn.clicked.connect(self.add_player) self.view.addWarBtn.clicked.connect(self.add_war) + self.view.on_edit_item = self.edit_item + self.view.on_delete_item = self.delete_item def on_app_close(self) -> bool: if self.is_dirty: @@ -120,6 +123,13 @@ class Controller: wars = self.model.get_all_wars() self.view.display_wars(wars) + def refresh_views(self): + current = self.view.get_current_tab() + if current == "players": + self.refresh_players_view() + elif current == "wars": + self.refresh_wars_view() + def on_tree_selection_changed(self, selection): self.selected_war_id = None self.selected_campaign_id = None @@ -137,50 +147,86 @@ class Controller: 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 _validate_player_inputs(self, name: str) -> bool: + if not name.strip(): + QMessageBox.warning( + self.view, + "Invalid name", + "Player name cannot be empty." + ) + return False + return True + def add_player(self): dialog = PlayerDialog(self.view) result = dialog.exec() # modal blocking dialog if result == QDialog.DialogCode.Accepted: name = dialog.get_player_name() - if not name: - QMessageBox.warning( - self.view, - "Invalid name", - "Player name cannot be empty." - ) + if not self._validate_player_inputs(name): return self.model.add_player(name) self.is_dirty = True self.refresh_players_view() self.update_window_title() - + + def _validate_war_inputs(self, name: str, year: int) -> bool: + if not name.strip(): + QMessageBox.warning( + self.view, + "Invalid name", + "War name cannot be empty." + ) + return False + if not (1970 <= year <= 3000): + QMessageBox.warning( + self.view, + "Invalid year", + "Year must be between 1970 and 3000." + ) + return False + return True + def add_war(self): - dialog = WarDialog(self.view) + dialog = WarDialog(self.view, default_year=self.model.get_default_war_values()["year"]) result = dialog.exec() # modal blocking dialog if result == QDialog.DialogCode.Accepted: name = dialog.get_war_name() - if not name: - QMessageBox.warning( - self.view, - "Invalid name", - "War name cannot be empty." - ) + year = dialog.get_war_year() + if not self._validate_war_inputs(name, year): return - self.model.add_war(name) + self.model.add_war(name, year) self.is_dirty = True self.refresh_wars_view() self.update_window_title() + def _validate_campaign_inputs(self, name: str, month: int) -> bool: + if not name.strip(): + QMessageBox.warning( + self.view, + "Invalid name", + "Campaign name cannot be empty." + ) + return False + if not (1 <= month <= 12): + QMessageBox.warning( + self.view, + "Invalid month", + "Month must be between 1 and 12." + ) + return False + return True + def add_campaign(self): if not self.selected_war_id: return - dialog = CampaignDialog(self.view) + dialog = CampaignDialog(self.view, default_month=self.model.get_default_campaign_values(self.selected_war_id)["month"]) if dialog.exec() != QDialog.DialogCode.Accepted: return name = dialog.get_campaign_name() - if not name: + month = dialog.get_campaign_month() + if not self._validate_campaign_inputs(name, month): return - self.model.add_campaign(self.selected_war_id, name) + self.model.add_campaign(self.selected_war_id, name, month) self.is_dirty = True self.refresh_wars_view() self.update_window_title() @@ -192,3 +238,54 @@ class Controller: self.is_dirty = True self.refresh_wars_view() self.update_window_title() + + def edit_item(self, item_type: str, item_id: str): + if item_type == "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() + if not self._validate_player_inputs(name): + return + self.model.update_player(item_id, name=name) + elif item_type == "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() + if not self._validate_war_inputs(name, year): + return + self.model.update_war(item_id, name=name, year=year) + elif item_type == "campaign": + camp = self.model.get_campaign(item_id) + 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 not self._validate_campaign_inputs(name, month): + return + self.model.update_campaign(item_id, name=name, month=month) + self.is_dirty = True + self.refresh_views() + + def delete_item(self, item_type: str, item_id: str): + reply = QMessageBox.question( + self.view, + "Confirm deletion", + "Are you sure you want to delete this item?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + if reply != QMessageBox.StandardButton.Yes: + return + if item_type == "player": + self.model.remove_player(item_id) + elif item_type == "war": + self.model.remove_war(item_id) + elif item_type == "campaign": + self.model.remove_campaign(item_id) + elif item_type == "round": + self.model.remove_round(item_id) + self.is_dirty = True + self.refresh_views() + diff --git a/src/warchron/model/campaign.py b/src/warchron/model/campaign.py index 4006a06..66dd16d 100644 --- a/src/warchron/model/campaign.py +++ b/src/warchron/model/campaign.py @@ -4,10 +4,10 @@ from datetime import datetime from warchron.model.round import Round class Campaign: - def __init__(self, name): + def __init__(self, name, month): self.id = str(uuid4()) self.name = name - self.month = datetime.now().month + self.month = month self.entrants = {} self.rounds = [] self.is_over = False @@ -36,20 +36,14 @@ class Campaign: @staticmethod def fromDict(data: dict): - camp = Campaign(name=data["name"]) + camp = Campaign(name=data["name"], month=data["month"]) camp.set_id(data["id"]) - camp.set_month(data["month"]) # camp.entrants = data.get("entrants", {}) for rnd_data in data.get("rounds", []): camp.rounds.append(Round.fromDict(rnd_data)) camp.set_state(data.get("is_over", False)) return camp - def add_round(self, number: int) -> Round: - round = Round() - self.rounds.append(round) - return round - def get_round(self, round_id) -> Round: return self.rounds[round_id] @@ -59,4 +53,9 @@ class Campaign: def add_round(self) -> Round: round = Round() self.rounds.append(round) - return round \ No newline at end of file + return round + + def remove_round(self, round_id: str): + rnd = next((r for r in self.rounds if r.id == round_id), None) + if rnd: + self.rounds.remove(rnd) diff --git a/src/warchron/model/model.py b/src/warchron/model/model.py index aa05ec2..f488eed 100644 --- a/src/warchron/model/model.py +++ b/src/warchron/model/model.py @@ -1,6 +1,7 @@ from pathlib import Path import json import shutil +from datetime import datetime from warchron.model.player import Player from warchron.model.war import War @@ -60,8 +61,8 @@ class Model: def get_player(self, id): return self.players[id] - def update_player(self, id, name): - player = self.get_player(id) + def update_player(self, player_id: str, *, name: str): + player = self.get_player(player_id) player.set_name(name) def delete_player(self, id): @@ -70,20 +71,41 @@ class Model: def get_all_players(self) -> list[Player]: return list(self.players.values()) - def add_war(self, name) -> War: - war = War(name) + def get_default_war_values(self) -> dict: + return { + "year": datetime.now().year + } + + def add_war(self, name: str, year: int) -> War: + war = War(name, year) self.wars[war.id] = war return war def get_war(self, id) -> War: return self.wars[id] + + def get_war_by_campaign(self, campaign_id: str) -> War: + for war in self.wars.values(): + for camp in war.campaigns: + if camp.id == campaign_id: + return war + raise KeyError(f"Campaign {campaign_id} not found in any War") + + def update_war(self, war_id: str, *, name: str, year: int): + war = self.get_war(war_id) + war.set_name(name) + war.set_year(year) def get_all_wars(self) -> list[War]: return list(self.wars.values()) - - def add_campaign(self, war_id: str, name: str) -> Campaign: + + def get_default_campaign_values(self, war_id: str) -> dict: war = self.get_war(war_id) - return war.add_campaign(name) + return war.get_default_campaign_values() + + def add_campaign(self, war_id: str, name: str, month: int) -> Campaign: + war = self.get_war(war_id) + return war.add_campaign(name, month) def get_campaign(self, campaign_id) -> Campaign: for war in self.wars.values(): @@ -92,6 +114,17 @@ class Model: return campaign raise KeyError("Campaign not found") + def get_campaign_by_round(self, round_id: str) -> Campaign: + for war in self.wars.values(): + camp = war.get_campaign_by_round(round_id) + if camp is not None: + return camp + raise KeyError(f"Round {round_id} not found") + + def update_campaign(self, campaign_id: str, *, name: str, month: int): + war = self.get_war_by_campaign(campaign_id) + war.update_campaign(campaign_id, name=name, month=month) + def add_round(self, campaign_id: str) -> Round: campaign = self.get_campaign(campaign_id) return campaign.add_round() @@ -102,4 +135,18 @@ class Model: for rnd in campaign.rounds: if rnd.id == round_id: return rnd - raise KeyError("Round not found") \ No newline at end of file + raise KeyError("Round not found") + + def remove_player(self, player_id: str): + del self.players[player_id] + + def remove_war(self, war_id: str): + del self.wars[war_id] + + def remove_campaign(self, campaign_id: str): + war = self.get_war_by_campaign(campaign_id) + war.remove_campaign(campaign_id) + + def remove_round(self, round_id: str): + camp = self.get_campaign_by_round(round_id) + camp.remove_round(round_id) \ No newline at end of file diff --git a/src/warchron/model/war.py b/src/warchron/model/war.py index 35a1fab..0633564 100644 --- a/src/warchron/model/war.py +++ b/src/warchron/model/war.py @@ -4,10 +4,10 @@ from datetime import datetime from warchron.model.campaign import Campaign class War: - def __init__(self, name): + def __init__(self, name, year): self.id = str(uuid4()) self.name = name - self.year = datetime.now().year + self.year = year self.entrants = {} self.campaigns = [] self.is_over = False @@ -36,27 +36,47 @@ class War: @staticmethod def fromDict(data: dict): - war = War(name=data["name"]) + war = War(name=data["name"], year=data["year"]) war.set_id(data["id"]) - war.set_year(data["year"]) # war.entrants = data.get("entrants", {}) for camp_data in data.get("campaigns", []): war.campaigns.append(Campaign.fromDict(camp_data)) war.set_state(data.get("is_over", False)) return war - def add_campaign(self, name) -> Campaign: - campaign = Campaign(name) + def get_default_campaign_values(self) -> dict: + return { + "month": datetime.now().month + } + + def add_campaign(self, name: str, month: int | None = None) -> Campaign: + if month is None: + month = self.get_default_campaign_values()["month"] + campaign = Campaign(name, month) self.campaigns.append(campaign) return campaign def get_campaign(self, campaign_id) -> Campaign: - return self.campaigns[campaign_id] + for camp in self.campaigns: + if camp.id == campaign_id: + return camp + raise KeyError(f"Campaign {campaign_id} not found in War {self.id}") + def get_campaign_by_round(self, round_id: str) -> Campaign: + for camp in self.campaigns: + for rnd in camp.rounds: + if rnd.id == round_id: + return camp + raise KeyError(f"Round {round_id} not found in any Campaign") + + def update_campaign(self, campaign_id: str, *, name: str, month: int): + camp = self.get_campaign(campaign_id) + camp.set_name(name) + camp.set_month(month) + def get_all_campaigns(self) -> list[Campaign]: return list(self.campaigns) - def add_campaign(self, name: str) -> Campaign: - campaign = Campaign(name) - self.campaigns.append(campaign) - return campaign \ No newline at end of file + def remove_campaign(self, campaign_id: str): + camp = self.get_campaign(campaign_id) + self.campaigns.remove(camp) \ No newline at end of file diff --git a/src/warchron/view/ui/ui_campaign_dialog.py b/src/warchron/view/ui/ui_campaign_dialog.py index 2482d5b..8922ddf 100644 --- a/src/warchron/view/ui/ui_campaign_dialog.py +++ b/src/warchron/view/ui/ui_campaign_dialog.py @@ -28,6 +28,14 @@ class Ui_campaignDialog(object): self.campaignName = QtWidgets.QLineEdit(parent=campaignDialog) self.campaignName.setGeometry(QtCore.QRect(60, 20, 113, 20)) self.campaignName.setObjectName("campaignName") + self.label_2 = QtWidgets.QLabel(parent=campaignDialog) + self.label_2.setGeometry(QtCore.QRect(10, 50, 47, 14)) + self.label_2.setObjectName("label_2") + self.campaignMonth = QtWidgets.QSpinBox(parent=campaignDialog) + self.campaignMonth.setGeometry(QtCore.QRect(60, 50, 71, 22)) + self.campaignMonth.setMinimum(1) + self.campaignMonth.setMaximum(12) + self.campaignMonth.setObjectName("campaignMonth") self.retranslateUi(campaignDialog) self.buttonBox.accepted.connect(campaignDialog.accept) # type: ignore @@ -38,6 +46,7 @@ class Ui_campaignDialog(object): _translate = QtCore.QCoreApplication.translate campaignDialog.setWindowTitle(_translate("campaignDialog", "Campaign")) self.label.setText(_translate("campaignDialog", "Name:")) + self.label_2.setText(_translate("campaignDialog", "Month:")) if __name__ == "__main__": diff --git a/src/warchron/view/ui/ui_campaign_dialog.ui b/src/warchron/view/ui/ui_campaign_dialog.ui index c5ef227..cbccab3 100644 --- a/src/warchron/view/ui/ui_campaign_dialog.ui +++ b/src/warchron/view/ui/ui_campaign_dialog.ui @@ -59,6 +59,35 @@ + + + + 10 + 50 + 47 + 14 + + + + Month: + + + + + + 60 + 50 + 71 + 22 + + + + 1 + + + 12 + + diff --git a/src/warchron/view/ui/ui_war_dialog.py b/src/warchron/view/ui/ui_war_dialog.py index 46404e7..a25cff0 100644 --- a/src/warchron/view/ui/ui_war_dialog.py +++ b/src/warchron/view/ui/ui_war_dialog.py @@ -13,12 +13,12 @@ class Ui_warDialog(object): def setupUi(self, warDialog): warDialog.setObjectName("warDialog") warDialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) - warDialog.resize(378, 98) + warDialog.resize(378, 124) icon = QtGui.QIcon() icon.addPixmap(QtGui.QPixmap(".\\src\\warchron\\view\\ui\\../resources/warchron_logo.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) warDialog.setWindowIcon(icon) self.buttonBox = QtWidgets.QDialogButtonBox(parent=warDialog) - self.buttonBox.setGeometry(QtCore.QRect(10, 60, 341, 32)) + self.buttonBox.setGeometry(QtCore.QRect(10, 80, 341, 32)) self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal) self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Ok) self.buttonBox.setObjectName("buttonBox") @@ -28,6 +28,14 @@ class Ui_warDialog(object): self.warName = QtWidgets.QLineEdit(parent=warDialog) self.warName.setGeometry(QtCore.QRect(60, 20, 113, 20)) self.warName.setObjectName("warName") + self.label_2 = QtWidgets.QLabel(parent=warDialog) + self.label_2.setGeometry(QtCore.QRect(10, 50, 47, 14)) + self.label_2.setObjectName("label_2") + self.warYear = QtWidgets.QSpinBox(parent=warDialog) + self.warYear.setGeometry(QtCore.QRect(60, 50, 71, 22)) + self.warYear.setMinimum(1970) + self.warYear.setMaximum(3000) + self.warYear.setObjectName("warYear") self.retranslateUi(warDialog) self.buttonBox.accepted.connect(warDialog.accept) # type: ignore @@ -38,6 +46,7 @@ class Ui_warDialog(object): _translate = QtCore.QCoreApplication.translate warDialog.setWindowTitle(_translate("warDialog", "War")) self.label.setText(_translate("warDialog", "Name:")) + self.label_2.setText(_translate("warDialog", "Year:")) if __name__ == "__main__": diff --git a/src/warchron/view/ui/ui_war_dialog.ui b/src/warchron/view/ui/ui_war_dialog.ui index 89ae183..bd671d8 100644 --- a/src/warchron/view/ui/ui_war_dialog.ui +++ b/src/warchron/view/ui/ui_war_dialog.ui @@ -10,7 +10,7 @@ 0 0 378 - 98 + 124 @@ -24,7 +24,7 @@ 10 - 60 + 80 341 32 @@ -59,6 +59,35 @@ + + + + 10 + 50 + 47 + 14 + + + + Year: + + + + + + 60 + 50 + 71 + 22 + + + + 1970 + + + 3000 + + diff --git a/src/warchron/view/view.py b/src/warchron/view/view.py index d44615f..c7942f6 100644 --- a/src/warchron/view/view.py +++ b/src/warchron/view/view.py @@ -3,7 +3,7 @@ import calendar from PyQt6 import QtWidgets from PyQt6.QtCore import Qt -from PyQt6.QtWidgets import QDialog, QFileDialog, QTreeWidgetItem +from PyQt6.QtWidgets import QDialog, QFileDialog, QTreeWidgetItem, QMenu from PyQt6.QtGui import QCloseEvent from warchron.view.ui.ui_main_window import Ui_MainWindow @@ -22,7 +22,13 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): self.on_selection_changed = None self.on_add_campaign = None self.on_add_round = None + self.on_edit_item = None + self.on_delete_item = None + self.playersTable.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.playersTable.customContextMenuRequested.connect(self._on_players_table_context_menu) self.warsTree.currentItemChanged.connect(self._emit_selection_changed) + self.warsTree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.warsTree.customContextMenuRequested.connect(self._on_wars_tree_context_menu) self.addCampaignBtn.clicked.connect(self._on_add_campaign_clicked) self.addRoundBtn.clicked.connect(self._on_add_round_clicked) @@ -36,7 +42,42 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): "type": current.data(0, ROLE_TYPE), "id": current.data(0, ROLE_ID), }) - + + def _on_players_table_context_menu(self, pos): + item = self.playersTable.itemAt(pos) + if not item: + return + row = item.row() + player_item = self.playersTable.item(row, 0) + player_id = player_item.data(Qt.ItemDataRole.UserRole) + menu = QMenu(self) + edit_action = menu.addAction("Edit") + delete_action = menu.addAction("Delete") + action = menu.exec(self.playersTable.viewport().mapToGlobal(pos)) + if action == edit_action and self.on_edit_item: + self.on_edit_item("player", player_id) + elif action == delete_action and self.on_delete_item: + self.on_delete_item("player", player_id) + + def _on_wars_tree_context_menu(self, pos): + item = self.warsTree.itemAt(pos) + if not item: + return + item_type = item.data(0, ROLE_TYPE) + item_id = item.data(0, ROLE_ID) + menu = QMenu(self) + edit_action = None + if item_type != "round": + edit_action = menu.addAction("Edit") + delete_action = menu.addAction("Delete") + action = menu.exec(self.warsTree.viewport().mapToGlobal(pos)) + if action == edit_action: + if self.on_edit_item: + self.on_edit_item(item_type, item_id) + elif action == delete_action: + if self.on_delete_item: + self.on_delete_item(item_type, item_id) + def _on_add_campaign_clicked(self): if self.on_add_campaign: self.on_add_campaign() @@ -75,8 +116,12 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): table = self.playersTable table.setRowCount(len(players)) for row, player in enumerate(players): - table.setItem(row, 0, QtWidgets.QTableWidgetItem(player.name)) - table.setItem(row, 1, QtWidgets.QTableWidgetItem(player.id)) + name_item = QtWidgets.QTableWidgetItem(player.name) + name_item.setData(Qt.ItemDataRole.UserRole, player.id) + table.setItem(row, 0, name_item) + id_item = QtWidgets.QTableWidgetItem(player.id) + id_item.setFlags(id_item.flags() & ~Qt.ItemFlag.ItemIsEditable) + table.setItem(row, 1, id_item) table.resizeColumnsToContents() def display_wars(self, wars: list): @@ -116,29 +161,50 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): def set_add_round_enabled(self, enabled: bool): self.addRoundBtn.setEnabled(enabled) + def get_current_tab(self) -> str: + index = self.tabWidget.currentIndex() + if index == 0: + return "players" + elif index == 1: + return "wars" + return "" + class PlayerDialog(QDialog): - def __init__(self, parent=None): + def __init__(self, parent=None, *, default_name: str = ""): super().__init__(parent) self.ui = Ui_playerDialog() self.ui.setupUi(self) + self.ui.playerName.setText(default_name) def get_player_name(self) -> str: return self.ui.playerName.text().strip() class WarDialog(QDialog): - def __init__(self, parent=None): + def __init__(self, parent=None, default_name: str = "", default_year: int | None = None): super().__init__(parent) self.ui = Ui_warDialog() self.ui.setupUi(self) + self.ui.warName.setText(default_name) + if default_year is not None: + self.ui.warYear.setValue(default_year) def get_war_name(self) -> str: return self.ui.warName.text().strip() + + def get_war_year(self) -> int: + return int(self.ui.warYear.value()) class CampaignDialog(QDialog): - def __init__(self, parent=None): + def __init__(self, parent=None, default_name: str = "", default_month: int | None = None): super().__init__(parent) self.ui = Ui_campaignDialog() self.ui.setupUi(self) + self.ui.campaignName.setText(default_name) + if default_month is not None: + self.ui.campaignMonth.setValue(default_month) def get_campaign_name(self) -> str: return self.ui.campaignName.text().strip() + + def get_campaign_month(self) -> int: + return int(self.ui.campaignMonth.value()) \ No newline at end of file