diff --git a/build.py b/build.py new file mode 100644 index 0000000..a0ddd2f --- /dev/null +++ b/build.py @@ -0,0 +1,18 @@ +""" +Package application using zipapp into an executable zip archive +""" +import os +import zipapp + +INTERPRETER = '/usr/bin/env python3' +SOURCE_PATH = 'dsst' +TARGET_FILENAME = 'dsst' + +# The bundled file should be placed into the build directory +target_path = os.path.join(os.path.dirname(__file__), 'build') +# Make sure it exists +if not os.path.isdir(target_path): + os.mkdir(target_path) +target = os.path.join(target_path, TARGET_FILENAME) +# Create archive +zipapp.create_archive(source=SOURCE_PATH, target=target, interpreter=INTERPRETER) diff --git a/dsst/__main__.py b/dsst/__main__.py new file mode 100644 index 0000000..4475530 --- /dev/null +++ b/dsst/__main__.py @@ -0,0 +1,10 @@ +import sys +import os.path +# Add current directory to python path +path = os.path.realpath(os.path.abspath(__file__)) +sys.path.insert(0, os.path.dirname(path)) + +from dsst_gtk3 import gtk_ui + +if __name__ == '__main__': + gtk_ui.main() \ No newline at end of file diff --git a/dsst/dsst_gtk3/dialogs.py b/dsst/dsst_gtk3/dialogs.py index 2356ed8..6bcbcb6 100644 --- a/dsst/dsst_gtk3/dialogs.py +++ b/dsst/dsst_gtk3/dialogs.py @@ -3,6 +3,7 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk from datetime import datetime from dsst_sql import sql +from dsst_gtk3 import util def enter_string_dialog(builder: Gtk.Builder, title: str, value=None) -> str: @@ -105,3 +106,41 @@ def show_manage_drinks_dialog(builder: Gtk.Builder): result = dialog.run() dialog.hide() return result + + +def show_edit_death_dialog(builder: Gtk.Builder, episode_id: int, death: sql.Death=None): + dialog = builder.get_object("edit_death_dialog") # type: Gtk.Dialog + dialog.set_transient_for(builder.get_object("main_window")) + with sql.connection.atomic(): + if death: + index = util.Util.get_index_of_combo_model(builder.get_object('edit_death_enemy_combo'), 0, death.enemy.id) + builder.get_object('edit_death_enemy_combo').set_active(index) + + # TODO Default drink should be set in config + default_drink = sql.Drink.get().name + store = builder.get_object('player_penalties_store') + store.clear() + for player in builder.get_object('episode_players_store'): + store.append([None, player[1], default_drink, player[0]]) + + # Run the dialog + result = dialog.run() + dialog.hide() + + if result != Gtk.ResponseType.OK: + sql.connection.rollback() + return False + # Collect info from widgets and save to database + player_id = util.Util.get_combo_value(builder.get_object('edit_death_player_combo'), 0) + enemy_id = util.Util.get_combo_value(builder.get_object('edit_death_enemy_combo'), 3) + comment = builder.get_object('edit_death_comment_entry').get_text() + if not death: + death = sql.Death.create(episode=episode_id, player=player_id, enemy=enemy_id, info=comment) + + store = builder.get_object('player_penalties_store') + size = builder.get_object('edit_death_size_spin').get_value() + for entry in store: + drink_id = sql.Drink.get(sql.Drink.name == entry[2]) + sql.Penalty.create(size=size, player=entry[3], death=death.id, drink=drink_id) + + return True diff --git a/dsst/dsst_gtk3/gtk_ui.py b/dsst/dsst_gtk3/gtk_ui.py index 9f524af..61dfaa7 100644 --- a/dsst/dsst_gtk3/gtk_ui.py +++ b/dsst/dsst_gtk3/gtk_ui.py @@ -9,14 +9,18 @@ from dsst_gtk3 import util from dsst_sql import sql, sql_func -class DSSTGtkUi: +class GtkUi: """ The main UI class """ def __init__(self): # Load Glade UI files self.ui = Gtk.Builder() - self.ui.add_from_file(os.path.join(os.path.dirname(__file__), 'resources', 'glade', 'window.glade')) - self.ui.add_from_file(os.path.join(os.path.dirname(__file__), 'resources', 'glade', 'dialogs.glade')) + glade_resources = [ + ['dsst_gtk3', 'resources', 'glade', 'window.glade'], + ['dsst_gtk3', 'resources', 'glade', 'dialogs.glade'] + ] + for path in glade_resources: + self.ui.add_from_string(util.Util.load_ui_resource_string(path)) # Connect signal handlers to UI self.handlers = handlers.Handlers(self) self.ui.connect_signals(self.handlers) @@ -45,14 +49,20 @@ class DSSTGtkUi: for season in sql.Season.select().order_by(sql.Season.number): store.append([season.id, season.game_name]) - def reload_for_season(self, season_id): + # Reload after season was changed ################################################################################## + def reload_for_season(self): + season_id = self.get_selected_season_id() if season_id is None or season_id == -1: return # Rebuild episodes store + ep_id = self.get_selected_episode_id() + selection = self.ui.get_object('episodes_tree_view').get_selection() + # selection.handler_block_by_func(self.handlers.on_selected_episode_changed) store = self.ui.get_object('episodes_store') store.clear() for episode in sql_func.get_episodes_for_season(season_id): store.append([episode.id, episode.number, str(episode.date)]) + # Load player stats for season player_stats = {} for episode in sql_func.get_episodes_for_season(season_id): @@ -64,20 +74,31 @@ class DSSTGtkUi: for name, stats in player_stats.items(): store.append([name, stats[0], stats[1]]) # Load enemy stats for season - enemy_stats = {enemy.name: [0, 0] for enemy in sql.Season.get(sql.Season.id == season_id).enemies} + enemy_stats = {enemy.name: [0, 0, enemy.id] for enemy in sql.Season.get(sql.Season.id == season_id).enemies} store = self.ui.get_object('enemy_season_store') store.clear() for name, stats in enemy_stats.items(): - store.append([name, stats[0], stats[1]]) + store.append([name, stats[0], stats[1], stats[2]]) - def reload_for_episode(self, episode_id): - pass + # Reload after episode was changed ################################################################################# + def reload_for_episode(self): + episode_id = self.get_selected_episode_id() + if not episode_id: + return + store = self.ui.get_object('episode_players_store') + store.clear() + for player in sql.Episode.get(sql.Episode.id == self.get_selected_episode_id()).players: + store.append([player.id, player.name, player.hex_id]) def get_selected_season_id(self): season_id = util.Util.get_combo_value(self.ui.get_object('season_combo_box'), 0) return season_id if season_id != -1 else None + def get_selected_episode_id(self): + (model, tree_iter) = self.ui.get_object('episodes_tree_view').get_selection().get_selected() + return model.get_value(tree_iter, 0) if tree_iter else None -if __name__ == '__main__': - DSSTGtkUi() + +def main(): + GtkUi() Gtk.main() diff --git a/dsst/dsst_gtk3/handlers/center_handlers.py b/dsst/dsst_gtk3/handlers/center_handlers.py index 6e41715..290ef8b 100644 --- a/dsst/dsst_gtk3/handlers/center_handlers.py +++ b/dsst/dsst_gtk3/handlers/center_handlers.py @@ -1,10 +1,15 @@ -from dsst_gtk3.gtk_ui import DSSTGtkUi -from dsst_sql import sql -from dsst_gtk3 import dialogs, util +from dsst_gtk3 import dialogs, gtk_ui + class CenterHandlers: - def __init__(self, app: DSSTGtkUi): + def __init__(self, app: 'gtk_ui.GtkUi'): self.app = app def do_add_death(self, *_): - pass \ No newline at end of file + ep_id = self.app.get_selected_episode_id() + result = dialogs.show_edit_death_dialog(self.app.ui, ep_id) + if result: + self.app.reload_for_season() + + def on_penalty_drink_changed(self, widget, path, text): + self.app.ui.get_object('player_penalties_store')[path][2] = text \ No newline at end of file diff --git a/dsst/dsst_gtk3/handlers/dialog_handlers.py b/dsst/dsst_gtk3/handlers/dialog_handlers.py index 10087d4..153f812 100644 --- a/dsst/dsst_gtk3/handlers/dialog_handlers.py +++ b/dsst/dsst_gtk3/handlers/dialog_handlers.py @@ -1,10 +1,9 @@ -from dsst_gtk3.gtk_ui import DSSTGtkUi -from dsst_gtk3 import dialogs, util +from dsst_gtk3 import dialogs, util, gtk_ui from dsst_sql import sql class DialogHandlers: - def __init__(self, app: DSSTGtkUi): + def __init__(self, app: 'gtk_ui.GtkUi'): self.app = app def do_add_player_to_episode(self, combo): @@ -20,7 +19,7 @@ class DialogHandlers: store.append([player_id, player.name, player.hex_id]) def do_add_enemy(self, entry): - if entry.get_text: + if entry.get_text(): store = self.app.ui.get_object('enemy_season_store') enemy = sql.Enemy.create(name=entry.get_text(), season=self.app.get_selected_season_id()) store.append([enemy.name, False, 0]) @@ -30,7 +29,7 @@ class DialogHandlers: result = dialogs.show_manage_drinks_dialog(self.app.ui) def do_add_drink(self, entry): - if entry.get_text: + if entry.get_text(): store = self.app.ui.get_object('drink_store') drink = sql.Drink.create(name=entry.get_text(), vol='0') store.append([drink.id, drink.name, drink.vol]) diff --git a/dsst/dsst_gtk3/handlers/handlers.py b/dsst/dsst_gtk3/handlers/handlers.py index 0e9f215..5b2594e 100644 --- a/dsst/dsst_gtk3/handlers/handlers.py +++ b/dsst/dsst_gtk3/handlers/handlers.py @@ -6,6 +6,8 @@ from dsst_gtk3.handlers.players import PlayerHandlers from dsst_gtk3.handlers.dialog_handlers import DialogHandlers from dsst_gtk3.handlers.center_handlers import CenterHandlers +from dsst_sql import sql + class Handlers(LeftColumnHandlers, PlayerHandlers, DialogHandlers, CenterHandlers): """ Class containing all signal handlers for the GTK GUI """ @@ -25,4 +27,11 @@ class Handlers(LeftColumnHandlers, PlayerHandlers, DialogHandlers, CenterHandler """ Signal will be sent when app should close :param args: Arguments to the delete event """ - Gtk.main_quit() \ No newline at end of file + Gtk.main_quit() + + # DEBUG Functions ################################################################################################## + + @staticmethod + def do_delete_database(*_): + sql.drop_tables() + sql.create_tables() \ No newline at end of file diff --git a/dsst/dsst_gtk3/handlers/left_column_handlers.py b/dsst/dsst_gtk3/handlers/left_column_handlers.py index 4f27790..e2fa4ac 100644 --- a/dsst/dsst_gtk3/handlers/left_column_handlers.py +++ b/dsst/dsst_gtk3/handlers/left_column_handlers.py @@ -1,11 +1,9 @@ -from datetime import datetime -from dsst_gtk3.gtk_ui import DSSTGtkUi from dsst_sql import sql -from dsst_gtk3 import dialogs, util +from dsst_gtk3 import dialogs, gtk_ui class LeftColumnHandlers: - def __init__(self, app: DSSTGtkUi): + def __init__(self, app: 'gtk_ui.GtkUi'): self.app = app def do_add_season(self, *_): @@ -15,11 +13,14 @@ class LeftColumnHandlers: self.app.reload_seasons() def do_season_selected(self, *_): - self.app.reload_for_season(self.app.get_selected_season_id()) + self.app.reload_for_season() def do_add_episode(self, *_): season_id = self.app.get_selected_season_id() if not season_id: return episode = dialogs.show_episode_dialog(self.app.ui, 'Create new Episode', season_id) - self.app.reload_for_season(season_id) + self.app.reload_for_season() + + def on_selected_episode_changed(self, *_): + self.app.reload_for_episode() \ No newline at end of file diff --git a/dsst/dsst_gtk3/handlers/players.py b/dsst/dsst_gtk3/handlers/players.py index 5230b47..214936c 100644 --- a/dsst/dsst_gtk3/handlers/players.py +++ b/dsst/dsst_gtk3/handlers/players.py @@ -1,10 +1,9 @@ -from dsst_gtk3.gtk_ui import DSSTGtkUi -from dsst_gtk3 import dialogs, util +from dsst_gtk3 import dialogs, gtk_ui from dsst_sql import sql class PlayerHandlers: - def __init__(self, app: DSSTGtkUi): + def __init__(self, app: 'gtk_ui.GtkUi'): self.app = app def do_manage_players(self, *_): diff --git a/dsst/dsst_gtk3/resources/glade/window.glade b/dsst/dsst_gtk3/resources/glade/window.glade index 4a542d6..979ce8a 100644 --- a/dsst/dsst_gtk3/resources/glade/window.glade +++ b/dsst/dsst_gtk3/resources/glade/window.glade @@ -12,6 +12,171 @@ + + False + False + True + dialog + False + + + False + vertical + 4 + + + False + end + + + gtk-ok + True + True + True + True + + + True + True + 0 + + + + + gtk-cancel + True + True + True + True + + + True + True + 1 + + + + + False + False + 0 + + + + + True + False + vertical + 5 + + + True + False + 5 + + + True + False + 5 + 5 + Add Player + + + False + True + 0 + + + + + True + True + + + + False + True + 1 + + + + + False + True + 0 + + + + + True + False + 5 + 5 + All Players + 0 + + + False + True + 1 + + + + + True + True + True + all_players_store + 0 + + + + + + Name + + + + 1 + + + + + + + Hex ID + + + + 2 + + + + + + + False + True + 2 + + + + + True + True + 1 + + + + + + okButtonRename2 + cancelButtonRename2 + + + + + @@ -187,6 +352,200 @@ + + + + + + + + + + + + + + False + Manage Enemies For This Season + False + True + dialog + False + + + False + vertical + 4 + + + False + end + + + gtk-ok + True + True + True + True + + + True + True + 0 + + + + + gtk-cancel + True + True + True + True + + + True + True + 1 + + + + + False + False + 0 + + + + + True + False + vertical + 5 + + + True + False + 5 + + + True + False + 5 + 5 + Add Enemy + + + False + True + 0 + + + + + True + True + + + + False + True + 1 + + + + + False + True + 0 + + + + + True + False + 5 + 5 + All Enemies + 0 + + + False + True + 1 + + + + + True + True + True + enemy_season_store + 0 + + + + + + Name + + + + 0 + + + + + + + False + True + 2 + + + + + True + True + 1 + + + + + + okButtonRename1 + cancelButtonRename1 + + + + + + + 1000000 + 1 + 10 + + + + + + + + + + + + + + + + + + + + + + + False False @@ -331,6 +690,7 @@ True True True + False @@ -450,197 +810,6 @@ - - - - - - - - - - - - False - Manage Enemies For This Season - False - True - dialog - False - - - False - vertical - 4 - - - False - end - - - gtk-ok - True - True - True - True - - - True - True - 0 - - - - - gtk-cancel - True - True - True - True - - - True - True - 1 - - - - - False - False - 0 - - - - - True - False - vertical - 5 - - - True - False - 5 - - - True - False - 5 - 5 - Add Enemy - - - False - True - 0 - - - - - True - True - - - False - True - 1 - - - - - False - True - 0 - - - - - True - False - 5 - 5 - All Enemies - 0 - - - False - True - 1 - - - - - True - True - True - enemy_season_store - 0 - - - - - - Name - - - - 0 - - - - - - - False - True - 2 - - - - - True - True - 1 - - - - - - okButtonRename1 - cancelButtonRename1 - - - - - - - 1000000 - 1 - 10 - - - - - - - - - - - - - - - - - - - - - - - @@ -651,8 +820,21 @@ - + + + + + + + + + + + + + False + Edit Death Event False True dialog @@ -667,7 +849,7 @@ False end - + gtk-ok True True @@ -681,7 +863,7 @@ - + gtk-cancel True True @@ -718,7 +900,7 @@ False 5 5 - Add Player + Enemy False @@ -727,13 +909,21 @@ - + True - True + False + enemy_season_store + + + + 0 + + - False + True True + end 1 @@ -745,13 +935,44 @@ - + True False - 5 - 5 - All Players - 0 + + + True + False + 5 + 5 + 5 + 5 + Player + + + False + True + 0 + + + + + True + False + episode_players_store + + + + 1 + + + + + True + True + end + 1 + + False @@ -760,18 +981,93 @@ - + + True + False + + + True + False + 5 + 5 + 5 + 5 + Drink Size + + + False + True + 0 + + + + + True + True + + + False + True + end + 1 + + + + + False + True + 2 + + + + + True + False + + + True + False + Comment + + + False + True + 0 + + + + + True + True + + + True + True + end + 1 + + + + + False + True + 3 + + + + + 100 True True True - all_players_store + player_penalties_store 0 - Name + Player @@ -782,9 +1078,15 @@ - Hex ID + Penalty - + + True + False + drink_store + 1 + + 2 @@ -793,9 +1095,9 @@ - False + True True - 2 + 4 @@ -808,8 +1110,8 @@ - okButtonRename2 - cancelButtonRename2 + okButtonRename4 + cancelButtonRename4 @@ -964,6 +1266,29 @@ + + + True + False + Debug + True + + + True + False + + + True + False + Delete Database + True + + + + + + + False @@ -1035,6 +1360,7 @@ True False seasons_store + 0 1 @@ -1104,33 +1430,40 @@ - + True True - episodes_store - - - + in - - Date - - - - 2 - + + True + True + episodes_store + 0 + + + + - - - - - autosize - Episode - - - 1 - + + Date + + + + 2 + + + + + + + autosize + Episode + + + + @@ -1138,7 +1471,7 @@ True True - 4 + 3 @@ -1301,50 +1634,67 @@ - + True True - True - player_season_store - - - + in - - Name - - - - 0 - + + True + True + True + player_season_store + 0 + + - - - - - Deaths - - - 1 - + + Name + True + True + 0 + + + + 0 + + + - - - - - Victories - - - 2 - + + Deaths + True + True + 1 + + + + 1 + + + + + + + Victories + True + True + 2 + + + + 2 + + + - False + True True 1 @@ -1367,39 +1717,47 @@ - + True True - True - drink_store - - - + in - - Name - - - - 1 - + + True + True + True + drink_store + 0 + + - - - - - Vol. - - - 2 - + + Name + + + + 1 + + + + + + + Vol. + + + + 2 + + + - False + True True 3 @@ -1422,41 +1780,56 @@ - + True True - True - enemy_season_store - - - + in - - Name - - - - 1 - 0 - + + True + True + True + enemy_season_store + False + 0 + + - - - - - Attempts - - - 1 - 2 - + + Name + True + True + 0 + + + + 1 + 0 + + + + + + + Attempts + True + True + 1 + + + + 1 + 2 + + + - False + True True 5 diff --git a/dsst/dsst_gtk3/util.py b/dsst/dsst_gtk3/util.py index 5df5cd1..1c96755 100644 --- a/dsst/dsst_gtk3/util.py +++ b/dsst/dsst_gtk3/util.py @@ -1,3 +1,7 @@ +import os +from zipfile import ZipFile + + class Util: @staticmethod def get_combo_value(combo, index: int): @@ -7,3 +11,33 @@ class Util: return combo.get_model().get_value(tree_iter, index) else: return -1 + + @staticmethod + def get_index_of_combo_model(combo, column: int, value: int): + model = combo.get_model() + return [model.index(entry) for entry in model if entry[column] == value] + + @staticmethod + def load_ui_resource_string(resource_path: list) -> str: + """ Load content of Glade UI files from resources path + :param resource_path: List of directory names from 'dsst' base directory + :return: String content of the Glade file + """ + if os.path.isdir(os.path.dirname(__file__)): + return Util.load_ui_resource_from_file(resource_path) + else: + + return Util.load_ui_resource_from_archive(resource_path) + + @staticmethod + def load_ui_resource_from_file(resource_path: list) -> str: + project_base_dir = os.path.dirname(os.path.dirname(__file__)) + full_path = os.path.join(project_base_dir, *resource_path) + with open(full_path, 'r') as file: + return file.read() + + @staticmethod + def load_ui_resource_from_archive(resource_path: list) -> str: + zip_path = os.path.dirname(os.path.dirname(__file__)) + with ZipFile(zip_path, 'r') as archive: + return archive.read(str(os.path.join(*resource_path))).decode('utf-8') diff --git a/dsst/dsst_sql/sql.py b/dsst/dsst_sql/sql.py index 8edf43c..fb57799 100644 --- a/dsst/dsst_sql/sql.py +++ b/dsst/dsst_sql/sql.py @@ -58,13 +58,23 @@ class Death(Model): info = CharField(null=True) player = ForeignKeyField(Player) enemy = ForeignKeyField(Enemy) - penalty = ForeignKeyField(Drink) episode = ForeignKeyField(Episode, backref='deaths') class Meta: database = connection +class Penalty(Model): + id = AutoField() + size = DecimalField() + ForeignKeyField(Drink) + ForeignKeyField(Player, backref='penalties') + ForeignKeyField(Death, backref='penalties') + + class Meta: + database = connection + + class Victory(Model): id = AutoField() info = CharField(null=True) @@ -77,8 +87,11 @@ class Victory(Model): def create_tables(): - models = [Season, Episode, Player, Drink, Enemy, Death, Victory, Episode.players.get_through_model()] + models = [Season, Episode, Player, Drink, Enemy, Death, Victory, Penalty, Episode.players.get_through_model()] for model in models: model.create_table() +def drop_tables(): + models = [Season, Episode, Player, Drink, Enemy, Death, Victory, Penalty, Episode.players.get_through_model()] + connection.drop_tables(models)