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