diff --git a/README.md b/README.md index d117d04..eb6e4bb 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,33 @@ Over-engineered statistics tool for keeping track of a drinking game ## Running the application -### Build executable zip archive -Run build script +The application is split into an server and client part. +To run standalone on a single machine you will have to have a running +dsst-server that is connected to a mysql database. -`$ python3 ./build.py` +Using the client you can then connect to the server via its +specified port. +### Building +Run the build script with the desired option -The archive will be saved into the `build` folder. The file is completly standalone and can be run from anywhere. +`$ python3 ./build.py {gtk3|server|all}` -`$ ./build/dsst` +The archive(s) will be saved into the `build` folder. +To run either server or client, just execute the archive files. -### Run python script directly -`$ python3 ./dsst/__main__.py` +`$ ./build/dsst-server-0.1` + +To run the server. + +`$ ./build/dsst-gtk3-0.1` + +To run the GTK client. ## Dependencies -- GObject (Gtk3) +- Python 3 +### Client +- python-gi <= v3.16 (Gtk3) + +### Server - mysqlclient (Python Mysql Driver) - peewee (ORM Framework) diff --git a/build.py b/build.py index a0ddd2f..cf1a6fa 100644 --- a/build.py +++ b/build.py @@ -3,16 +3,60 @@ Package application using zipapp into an executable zip archive """ import os import zipapp +import sys + +import shutil 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') +CLIENT_VERSION = '0.1' +SERVER_VERSION = '0.1' + +try: + build_mode = sys.argv[1] +except IndexError: + print('No build mode specified') + sys.exit(0) + +print('Building Mode: {}'.format(build_mode)) + +path = os.path.dirname(__file__) +# Specify build path +BUILD_PATH = os.path.join(path, '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) +if not os.path.isdir(BUILD_PATH): + os.mkdir(BUILD_PATH) + + +def build(target_filename, folder_name, entry_point): + source_path = os.path.join(BUILD_PATH, 'source') + if os.path.isdir(source_path): + shutil.rmtree(source_path) + os.mkdir(source_path) + shutil.copytree(os.path.join(path, 'dsst', folder_name), os.path.join(source_path, folder_name)) + shutil.copytree(os.path.join(path, 'dsst', 'common'), os.path.join(source_path, 'common')) + archive_name = os.path.join(BUILD_PATH, target_filename) + zipapp.create_archive(source=source_path, target=archive_name, interpreter=INTERPRETER, + main=entry_point) + print('Created {}'.format(archive_name)) + shutil.rmtree(source_path) + + +def build_server(): + build('dsst-server-{}'.format(SERVER_VERSION), 'dsst_server', 'dsst_server.server:main') + + +def build_gtk3(): + build('dsst-gtk3-{}'.format(CLIENT_VERSION), 'dsst_gtk3', 'dsst_gtk3.gtk_ui:main') + + +build_modes = { + 'server': build_server, + 'gtk3': build_gtk3 +} + +if build_mode == 'all': + for mode, build_function in build_modes.items(): + build_function() +else: + build_modes[build_mode]() \ No newline at end of file diff --git a/dsst/dsst_sql/__init__.py b/dsst/common/__init__.py similarity index 100% rename from dsst/dsst_sql/__init__.py rename to dsst/common/__init__.py diff --git a/dsst/common/models.py b/dsst/common/models.py new file mode 100644 index 0000000..55dd14c --- /dev/null +++ b/dsst/common/models.py @@ -0,0 +1,85 @@ +class Season: + def __init__(self, arg={}): + self.id = arg.get('id') + self.number = arg.get('number') + self.game_name = arg.get('game_name') + self.start_date = arg.get('start_date') + self.end_date = arg.get('end_date') + + self.episodes = arg.get('episodes') + self.enemies = arg.get('enemies') + + +class Player: + def __init__(self, arg={}): + self.id = arg.get('id') + self.name = arg.get('name') + self.hex_id = arg.get('hex_id') + + self.deaths = arg.get('deaths') + self.victories = arg.get('victories') + self.penalties = arg.get('penalties') + + +class Episode: + def __init__(self, arg={}): + self.id = arg.get('id') + self.seq_number = arg.get('seq_number') + self.number = arg.get('number') + self.name = arg.get('name') + self.date = arg.get('date') + self.season = arg.get('season') + self.players = arg.get('players') + self.deaths = arg.get('deaths') + self.victories = arg.get('victories') + + +class Drink: + def __init__(self, arg={}): + self.id = arg.get('id') + self.name = arg.get('name') + self.vol = arg.get('vol') + + +class Enemy: + def __init__(self, arg={}): + self.id = arg.get('id') + self.name = arg.get('name') + self.boss = arg.get('boss') + self.season = arg.get('season') + + +class Death: + def __init__(self, arg={}): + self.id = arg.get('id') + self.info = arg.get('info') + self.player = arg.get('player') + self.enemy = arg.get('enemy') + self.episode = arg.get('episode') + self.penalties = arg.get('penalties') + self.time = arg.get('time') + + +class Penalty: + def __init__(self, arg={}): + self.id = arg.get('id') + self.size = arg.get('size') + self.drink = arg.get('drink') + self.player = arg.get('player') + self.death = arg.get('death') + + +class Victory: + def __init__(self, arg={}): + self.id = arg.get('id') + self.info = arg.get('info') + self.player = arg.get('player') + self.enemy = arg.get('enemy') + self.episode = arg.get('episode') + self.time = arg.get('time') + + +class SeasonStats: + def __init__(self, arg={}): + self.player_kd = arg.get('player_kd') + self.enemies = arg.get('enemies') diff --git a/dsst/common/util.py b/dsst/common/util.py new file mode 100644 index 0000000..8c52538 --- /dev/null +++ b/dsst/common/util.py @@ -0,0 +1,32 @@ +import struct + + +def send_msg(sock, msg): + # Prefix each message with a 4-byte length (network byte order) + msg = struct.pack('>I', len(msg)) + msg + sock.sendall(msg) + + +def recv_msg(sock): + # Read message length and unpack it into an integer + raw_msglen = recvall(sock, 4) + if not raw_msglen: + return None + msglen = struct.unpack('>I', raw_msglen)[0] + # Read the message data + return recvall(sock, msglen) + + +def recvall(sock, n): + # Helper function to recv n bytes or return None if EOF is hit + data = b'' + while len(data) < n: + packet = sock.recv(n - len(data)) + if not packet: + return None + data += packet + return data + + +def list_class_methods(class_obj): + return [name for name in dir(class_obj) if not name.startswith('__')] diff --git a/dsst/__main__.py b/dsst/dsst_gtk3/__main__.py similarity index 69% rename from dsst/__main__.py rename to dsst/dsst_gtk3/__main__.py index 4475530..dcb106a 100644 --- a/dsst/__main__.py +++ b/dsst/dsst_gtk3/__main__.py @@ -1,10 +1,11 @@ -import sys import os.path +import sys + # Add current directory to python path path = os.path.realpath(os.path.abspath(__file__)) -sys.path.insert(0, os.path.dirname(path)) +sys.path.insert(0, os.path.dirname(os.path.dirname(path))) from dsst_gtk3 import gtk_ui if __name__ == '__main__': - gtk_ui.main() \ No newline at end of file + gtk_ui.main() diff --git a/dsst/dsst_gtk3/client.py b/dsst/dsst_gtk3/client.py new file mode 100644 index 0000000..b7da4b3 --- /dev/null +++ b/dsst/dsst_gtk3/client.py @@ -0,0 +1,40 @@ +import pprint +import socket +from common import util, models +try: + import cPickle as pickle +except ImportError: + import pickle + + +class Access: + def __init__(self, conn_dict): + self.host = conn_dict.get('host') + self.port = conn_dict.get('port') + self.buffer = conn_dict.get('buffer_size') + self.auth_token = conn_dict.get('auth_token') + + def send_request(self, action: str, *args): + request = {'auth_token': self.auth_token, + 'action': action, + 'args': args} + request = pickle.dumps(request) + soc = socket.socket() + try: + soc.connect((self.host, self.port)) + util.send_msg(soc, request) + message = util.recv_msg(soc) + message = pickle.loads(message) + if not message.get('success'): + raise Exception(message.get('message')) + finally: + soc.close() + return message.get('data') + +if __name__ == '__main__': + access = Access({'host': 'europa', 'port': 12345, 'buffer_size': 1024, 'auth_token': 'a'}) + action = 'load_seasons' + response = access.send_request(action) + pp = pprint.PrettyPrinter(indent=1) + for s in response: + pp.pprint(s.__dict__) diff --git a/dsst/dsst_gtk3/dialogs.py b/dsst/dsst_gtk3/dialogs.py index 10a517a..3986e1c 100644 --- a/dsst/dsst_gtk3/dialogs.py +++ b/dsst/dsst_gtk3/dialogs.py @@ -1,12 +1,10 @@ """ This module contains UI functions for displaying different dialogs """ -import gi -gi.require_version('Gtk', '3.0') +import datetime from gi.repository import Gtk -from datetime import datetime -from dsst_sql import sql -from dsst_gtk3 import util +from common import models +from dsst_gtk3 import gtk_ui, util def enter_string_dialog(builder: Gtk.Builder, title: str, value=None) -> str: @@ -33,159 +31,122 @@ def enter_string_dialog(builder: Gtk.Builder, title: str, value=None) -> str: return value -def show_episode_dialog(builder: Gtk.Builder, title: str, season_id: int, episode: sql.Episode=None): - """ Shows a dialog to edit an episode - :param builder: GtkBuilder with loaded 'dialogs.glade' - :param title: Title of the dialog window - :param season_id: Season to witch the episode should be added - :param episode: (Optional) Existing episode to edit - :return True if changes where saved False if discarded - """ - # Set up the dialog - dialog = builder.get_object("edit_episode_dialog") # type: Gtk.Dialog - dialog.set_transient_for(builder.get_object("main_window")) - dialog.set_title(title) - with sql.db.atomic(): - if not episode: - nxt_number = len(sql.Season.get_by_id(season_id).episodes) + 1 - episode = sql.Episode.create(seq_number=nxt_number, number=nxt_number, date=datetime.today(), - season=season_id) - # Set episode number - builder.get_object("episode_no_spin_button").set_value(episode.number) - # Set episode date - builder.get_object('episode_calendar').select_month(episode.date.month, episode.date.year) - builder.get_object('episode_calendar').select_day(episode.date.day) - # Set participants for the episode - builder.get_object('episode_players_store').clear() - for player in episode.players: - builder.get_object('episode_players_store').append([player.id, player.name, player.hex_id]) +def edit_season(builder: 'Gtk.Builder', season: 'models.Season'=None): + if not season: + season = models.Season() + builder.get_object('season_number_spin').set_value(season.number or 1) + builder.get_object('season_game_entry').set_text(season.game_name or '') + builder.get_object('season_start_entry').set_text(season.start_date or '') + builder.get_object('season_end_entry').set_text(season.end_date or '') - result = dialog.run() - dialog.hide() - - if result != Gtk.ResponseType.OK: - sql.db.rollback() - return False - - # Save all changes to Database - player_ids = [row[0] for row in builder.get_object('episode_players_store')] - # Insert new Players - episode.players = sql.Player.select().where(sql.Player.id << player_ids) - # Update Date of the Episode - cal_value = builder.get_object('episode_calendar').get_date() - selected_date = datetime(*cal_value).date() - episode.date = selected_date, - episode.number = int(builder.get_object("episode_no_spin_button").get_value()) - episode.name = builder.get_object("episode_name_entry").get_text() - episode.save() - return True - - -def show_manage_players_dialog(builder: Gtk.Builder, title: str): - """Show a dialog for managing player base data. - :param builder: Gtk.Builder object - :param title: Title for the dialog - """ - dialog = builder.get_object("manage_players_dialog") # type: Gtk.Dialog - dialog.set_transient_for(builder.get_object("main_window")) - dialog.run() + dialog = builder.get_object('edit_season_dialog') + result = dialog.run() dialog.hide() + if result != Gtk.ResponseType.OK: + return None -def show_manage_enemies_dialog(builder: Gtk.Builder, season_id: int): - dialog = builder.get_object("manage_enemies_dialog") # type: Gtk.Dialog - dialog.set_transient_for(builder.get_object("main_window")) - dialog.run() + season.number = builder.get_object('season_number_spin').get_value() + season.game_name = builder.get_object('season_game_entry').get_text() + start_string = builder.get_object('season_start_entry').get_text() + if start_string: + season.start_date = datetime.datetime.strptime(start_string, '%Y-%m-%d') + end_string = builder.get_object('season_end_entry').get_text() + if end_string: + season.end_date = datetime.datetime.strptime(end_string, '%Y-%m-%d') + return season + + +def edit_episode(app: 'gtk_ui.GtkUi', season_id: int, episode: 'models.Episode'=None): + """Show an dialog to create or edit episodes + :param app: Reference to main UI application + :param season_id: Is of the season in which the episode appears + :param episode: Existing episode object to edit + :return: Edited episode object, or None if the process was canceled + """ + if not episode: + episode = models.Episode() + episode.date = datetime.datetime.today() + episode.number = 1 + episode.name = '' + episode.players = [] + + app.ui.get_object('episode_name_entry').set_text(episode.name) + app.ui.get_object('episode_no_spin_button').set_value(episode.number) + app.ui.get_object('episode_calendar').select_month(episode.date.month, episode.date.year) + app.ui.get_object('episode_calendar').select_day(episode.date.day) + app.ui.get_object('episode_players_store').clear() + for player in episode.players: + app.ui.get_object('episode_players_store').append([player.id, player.name, player.hex_id]) + + dialog = app.ui.get_object('edit_episode_dialog') # type: Gtk.Dialog + result = dialog.run() dialog.hide() + if result != Gtk.ResponseType.OK: + return None -def show_manage_drinks_dialog(builder: Gtk.Builder): - dialog = builder.get_object("manage_drinks_dialog") # type: Gtk.Dialog - dialog.set_transient_for(builder.get_object("main_window")) - dialog.run() + episode.name = app.ui.get_object('episode_name_entry').get_text() + episode.number = app.ui.get_object('episode_no_spin_button').get_value() + cal_value = app.ui.get_object('episode_calendar').get_date() + selected_date = datetime.datetime(*cal_value).date() + episode.date = selected_date + player_ids = [row[0] for row in app.ui.get_object('episode_players_store')] + episode.players = [app.get_by_id(app.players, player_id) for player_id in player_ids] + episode.season = season_id + return episode + + +def create_death(app: 'gtk_ui.GtkUi'): + """Show a dialog to create death events for an episode + :param app: Main Gtk application + :return: Death object or None if dialog was canceled + """ + # Run the dialog + dialog = app.ui.get_object("edit_death_dialog") # type: Gtk.Dialog + result = dialog.run() dialog.hide() + if result != Gtk.ResponseType.OK: + return None + + death = models.Death() + hour_spin = app.ui.get_object('death_hour_spin') + min_spin = app.ui.get_object('death_min_spin') + # Parse the inputs + death.time = datetime.time(int(hour_spin.get_value()), int(min_spin.get_value())) + death.enemy = util.get_combo_value(app.ui.get_object('edit_death_enemy_combo'), 4) + death.player = util.get_combo_value(app.ui.get_object('edit_death_player_combo'), 0) + death.info = app.ui.get_object('edit_death_comment_entry').get_text() + death.episode = app.get_selected_episode_id() + store = app.ui.get_object('player_penalties_store') + size = app.ui.get_object('edit_death_size_spin').get_value() + death.penalties = [] + for entry in store: + drink_id = [drink.id for drink in app.drinks.data if drink.name == entry[2]][0] + penalty = models.Penalty({'id': entry[0], 'size': size, 'drink': drink_id, 'player': entry[3]}) + death.penalties.append(penalty) + + return death -def show_edit_death_dialog(builder: Gtk.Builder, episode_id: int, death: sql.Death=None): - """Show a dialog for editing or creating death events. - :param builder: A Gtk.Builder object - :param episode_id: ID to witch the death event belongs to - :param death: (Optional) Death event witch should be edited - :return: Gtk.ResponseType of the dialog +def create_victory(app: 'gtk_ui.GtkUi'): + """Show a dialog for creating victory events + :param app: Reference to main gtk ui object + :return: Created victory object or None, if canceled """ - dialog = builder.get_object("edit_death_dialog") # type: Gtk.Dialog - dialog.set_transient_for(builder.get_object("main_window")) - with sql.db.atomic(): - if death: - index = 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) + dialog = app.ui.get_object('edit_victory_dialog') + result = dialog.run() + dialog.hide() + if result != Gtk.ResponseType.OK: + return None - # 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]]) + hour_spin = app.ui.get_object('vic_hour_spin') + min_spin = app.ui.get_object('vic_min_spin') + victory = models.Victory() + victory.episode = app.get_selected_episode_id() + victory.info = app.ui.get_object('victory_comment_entry').get_text() + victory.player = util.get_combo_value(app.ui.get_object('edit_victory_player_combo'), 0) + victory.enemy = util.get_combo_value(app.ui.get_object('edit_victory_enemy_combo'), 4) + victory.time = datetime.time(int(hour_spin.get_value()), int(min_spin.get_value())) - # Run the dialog - result = dialog.run() - dialog.hide() - if result != Gtk.ResponseType.OK: - sql.db.rollback() - return result - - # Collect info from widgets and save to database - player_id = util.get_combo_value(builder.get_object('edit_death_player_combo'), 0) - enemy_id = 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 result - - -def show_edit_victory_dialog(builder: Gtk.Builder, episode_id: int, victory: sql.Victory=None): - """Show a dialog for editing or creating victory events. - :param builder: A Gtk.Builder object - :param episode_id: ID to witch the victory event belongs to - :param victory: (Optional) Victory event witch should be edited - :return: Gtk.ResponseType of the dialog - """ - dialog = builder.get_object("edit_victory_dialog") # type: Gtk.Dialog - dialog.set_transient_for(builder.get_object("main_window")) - with sql.db.atomic(): - if victory: - infos = [['edit_victory_player_combo', victory.player.id], - ['edit_victory_enemy_combo', victory.enemy.id]] - for info in infos: - combo = builder.get_object(info[0]) - index = util.get_index_of_combo_model(combo, 0, info[1]) - combo.set_active(index) - builder.get_object('victory_comment_entry').set_text(victory.info) - - # Run the dialog - result = dialog.run() - dialog.hide() - if result != Gtk.ResponseType.OK: - sql.db.rollback() - return result - - # Collect info from widgets and save to database - player_id = util.get_combo_value(builder.get_object('edit_victory_player_combo'), 0) - enemy_id = util.get_combo_value(builder.get_object('edit_victory_enemy_combo'), 3) - comment = builder.get_object('victory_comment_entry').get_text() - if not victory: - sql.Victory.create(episode=episode_id, player=player_id, enemy=enemy_id, info=comment) - else: - victory.player = player_id - victory.enemy = enemy_id - victory.info = comment - victory.save() - - return result + return victory diff --git a/dsst/dsst_gtk3/gtk_ui.py b/dsst/dsst_gtk3/gtk_ui.py index e987820..abd54b9 100644 --- a/dsst/dsst_gtk3/gtk_ui.py +++ b/dsst/dsst_gtk3/gtk_ui.py @@ -1,11 +1,10 @@ -import gi import os +import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk from dsst_gtk3.handlers import handlers -from dsst_gtk3 import util, reload -from dsst_sql import sql, sql_func - +from dsst_gtk3 import util, reload, client +from common import models class GtkUi: """ The main UI class """ @@ -18,36 +17,90 @@ class GtkUi: ] for path in glade_resources: self.ui.add_from_string(util.load_ui_resource_string(path)) + # Set the status bar logo + dd_logo = ['dsst_gtk3', 'resources', 'images', 'dd.png'] + logo_pixbuf = util.load_image_resource(dd_logo, 60, 13) + logo = self.ui.get_object('status_bar_logo').set_from_pixbuf(logo_pixbuf) # Connect signal handlers to UI self.handlers = handlers.Handlers(self) self.ui.connect_signals(self.handlers) # Show all widgets self.ui.get_object('main_window').show_all() - db_config = config['sql_connections'][0] - # Initialize the database - sql.db.init(db_config['db_name'], host=db_config['host'], port=db_config['port'], - user=db_config['user'], password=db_config['password']) - # Show database info in status bar - self.set_db_status_label(db_config) - # Create database if not exists - sql_func.create_tables() - self.reload() + # Connect to data server + config = config['servers'][0] + self.data_client = client.Access(config) + # Create local data caches + self.players = util.Cache() + self.drinks = util.Cache() + self.seasons = util.Cache() + self.episodes = util.Cache() + self.enemies = util.Cache() + self.season_stats = util.Cache() + # Create meta data cache + self.meta = {'connection': '{}:{}'.format(config.get('host'), config.get('port'))} + # Load base data and seasons + self.load_server_meta() + self.full_reload() + self.update_status_bar_meta() + + def load_server_meta(self): + self.meta['database'] = self.data_client.send_request('load_db_meta') + + def full_reload(self): + with util.network_operation(self): + self.players.data = self.data_client.send_request('load_players') + self.drinks.data = self.data_client.send_request('load_drinks') + self.seasons.data = self.data_client.send_request('load_seasons') + season_id = self.get_selected_season_id() + if season_id: + self.episodes.data = self.data_client.send_request('load_episodes', season_id) + self.season_stats.data = self.data_client.send_request('load_season_stats', season_id) + cur_season = [s for s in self.seasons.data if s.id == season_id][0] + self.enemies.data = cur_season.enemies + reload.rebuild_view_data(self) def reload(self): - reload.reload_base_data(self.ui, self) - season_id = self.get_selected_season_id() - if season_id: - reload.reload_episodes(self.ui, self, season_id) - reload.reload_season_stats(self.ui, self, season_id) - else: - return - episode_id = self.get_selected_episode_id() - if episode_id: - reload.reload_episode_stats(self.ui, self, episode_id) + pass - def set_db_status_label(self, db_conf: dict): - self.ui.get_object('connection_label').set_text(f'{db_conf["user"]}@{db_conf["host"]}') - self.ui.get_object('db_label').set_text(f'{db_conf["db_name"]}') + def update_enemy(self, enemy: 'models.Enemy'): + with util.network_operation(self): + self.data_client.send_request('update_enemy', enemy) + self.full_reload() + + def update_player(self, player: 'models.Player'): + with util.network_operation(self): + self.data_client.send_request('update_player', player) + self.full_reload() + + def update_drink(self, drink: 'models.Drink'): + with util.network_operation(self): + self.data_client.send_request('update_drink', drink) + self.full_reload() + + def save_death(self, death: 'models.Death'): + with util.network_operation(self): + self.data_client.send_request('save_death', death) + self.full_reload() + + def save_victory(self, victory: 'models.Victory'): + with util.network_operation(self): + self.data_client.send_request('save_victory', victory) + self.full_reload() + + def update_season(self, season: 'models.Season'): + with util.network_operation(self): + self.data_client.send_request('update_season', season) + self.seasons.valid = False + + def update_episode(self, episode: 'models.Episode'): + with util.network_operation(self): + self.data_client.send_request('update_episode', episode) + self.episodes.valid = False + self.season_stats.valid = False + + def update_status_bar_meta(self): + self.ui.get_object('connection_label').set_text(self.meta.get('connection')) + self.ui.get_object('db_label').set_text(self.meta.get('database') or '') def get_selected_season_id(self) -> int: """Read ID of the selected season from the UI @@ -63,6 +116,13 @@ class GtkUi: (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 + @staticmethod + def get_by_id(cache: 'util.Cache', object_id: int): + try: + return [x for x in cache.data if x.id == object_id][0] + except KeyError: + return None + def main(): if not os.path.isfile(util.CONFIG_PATH): @@ -70,3 +130,7 @@ def main(): config = util.load_config(util.CONFIG_PATH) GtkUi(config) Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/dsst/dsst_gtk3/handlers/base_data_handlers.py b/dsst/dsst_gtk3/handlers/base_data_handlers.py index b2c4d61..15738a1 100644 --- a/dsst/dsst_gtk3/handlers/base_data_handlers.py +++ b/dsst/dsst_gtk3/handlers/base_data_handlers.py @@ -1,5 +1,5 @@ from dsst_gtk3 import dialogs, gtk_ui -from dsst_sql import sql +from common import models class BaseDataHandlers: @@ -7,48 +7,39 @@ class BaseDataHandlers: def __init__(self, app: 'gtk_ui.GtkUi'): self.app = app - def do_manage_players(self, *_): - dialogs.show_manage_players_dialog(self.app.ui, 'Manage Players') - def do_add_player(self, entry): if entry.get_text(): - sql.Player.create(name=entry.get_text()) + self.app.update_player(models.Player({'name': entry.get_text()})) entry.set_text('') - self.app.reload() - - def do_manage_enemies(self, *_): - dialogs.show_manage_enemies_dialog(self.app.ui, self.app.get_selected_season_id()) def on_player_name_edited(self, _, index, value): row = self.app.ui.get_object('all_players_store')[index] - sql.Player.update(name=value)\ - .where(sql.Player.id == row[0])\ - .execute() - self.app.reload() + player = models.Player({'id': row[0], + 'name': value, + 'hex_id': row[2]}) + self.app.update_player(player) def on_player_hex_edited(self, _, index, value): row = self.app.ui.get_object('all_players_store')[index] - sql.Player.update(hex_id=value)\ - .where(sql.Player.id == row[0])\ - .execute() - self.app.reload() + player = models.Player({'id': row[0], + 'name': row[1], + 'hex_id': value}) + self.app.update_player(player) def do_add_drink(self, entry): if entry.get_text(): - sql.Drink.create(name=entry.get_text(), vol=0) + drink = models.Drink({'name': entry.get_text(), 'vol': 0.00}) + self.app.update_drink(drink) entry.set_text('') - self.app.reload() def on_drink_name_edited(self, _, index, value): row = self.app.ui.get_object('drink_store')[index] - sql.Drink.update(name=value)\ - .where(sql.Drink.id == row[0])\ - .execute() - self.app.reload() + drink = [d for d in self.app.drinks.data if d.id == row[0]][0] + drink.name = value + self.app.update_drink(drink) def on_drink_vol_edited(self, _, index, value): row = self.app.ui.get_object('drink_store')[index] - sql.Drink.update(vol=value) \ - .where(sql.Drink.id == row[0]) \ - .execute() - self.app.reload() \ No newline at end of file + drink = [d for d in self.app.drinks.data if d.id == row[0]][0] + drink.vol = value + self.app.update_drink(drink) \ No newline at end of file diff --git a/dsst/dsst_gtk3/handlers/death_handlers.py b/dsst/dsst_gtk3/handlers/death_handlers.py index aa45acd..0522c4e 100644 --- a/dsst/dsst_gtk3/handlers/death_handlers.py +++ b/dsst/dsst_gtk3/handlers/death_handlers.py @@ -11,9 +11,9 @@ class DeathHandlers: ep_id = self.app.get_selected_episode_id() if not ep_id: return - result = dialogs.show_edit_death_dialog(self.app.ui, ep_id) - if result == Gtk.ResponseType.OK: - self.app.reload() + death = dialogs.create_death(self.app) + if death: + self.app.save_death(death) def on_penalty_drink_changed(self, _, path, text): self.app.ui.get_object('player_penalties_store')[path][2] = text diff --git a/dsst/dsst_gtk3/handlers/dialog_handlers.py b/dsst/dsst_gtk3/handlers/dialog_handlers.py index 5204828..14ca4ae 100644 --- a/dsst/dsst_gtk3/handlers/dialog_handlers.py +++ b/dsst/dsst_gtk3/handlers/dialog_handlers.py @@ -1,5 +1,8 @@ -from dsst_gtk3 import dialogs, util, gtk_ui -from dsst_sql import sql +import datetime + +from dsst_gtk3 import dialogs, util, gtk_ui, reload +from common import models +from gi.repository import Gtk class DialogHandlers: @@ -7,6 +10,11 @@ class DialogHandlers: def __init__(self, app: 'gtk_ui.GtkUi'): self.app = app + @staticmethod + def do_run_manage_dialog(dialog: 'Gtk.Dialog'): + dialog.run() + dialog.hide() + def do_add_player_to_episode(self, combo): """ Signal Handler for Add Player to Episode Button in Manage Episode Dialog :param combo: Combo box with all the available players @@ -14,7 +22,7 @@ class DialogHandlers: player_id = util.get_combo_value(combo, 0) if player_id: self.app.ui.get_object('add_player_combo_box').set_active(-1) - player = sql.Player.get(sql.Player.id == player_id) + player = self.app.get_by_id(self.app.players, player_id) store = self.app.ui.get_object('episode_players_store') if not any(row[0] == player_id for row in store): store.append([player_id, player.name, player.hex_id]) @@ -22,9 +30,41 @@ class DialogHandlers: def do_add_enemy(self, entry): 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, enemy.id]) + enemy = models.Enemy() + enemy.name = entry.get_text() + enemy.season = self.app.get_selected_season_id() + enemy.boss = not self.app.ui.get_object('enemy_optional_ckeck').get_active() + self.app.ui.get_object('enemy_optional_ckeck').set_active(False) entry.set_text('') - def do_manage_drinks(self, *_): - result = dialogs.show_manage_drinks_dialog(self.app.ui) + self.app.update_enemy(enemy) + + def on_enemy_name_edited(self, _, index, value): + row = self.app.ui.get_object('enemy_season_store')[index] + enemy = [enemy for enemy in self.app.enemies.data if enemy.id == row[4]][0] + enemy.name = value + self.app.update_enemy(enemy) + + def on_enemy_optional_edited(self, renderer, index): + new_optional_value = not renderer.get_active() + row = self.app.ui.get_object('enemy_season_store')[index] + enemy = [enemy for enemy in self.app.enemies.data if enemy.id == row[4]][0] + enemy.boss = new_optional_value + self.app.update_enemy(enemy) + + def do_show_date_picker(self, entry: 'Gtk.Entry', *_): + dialog = self.app.ui.get_object('date_picker_dialog') + result = dialog.run() + dialog.hide() + if result == Gtk.ResponseType.OK: + date = self.app.ui.get_object('date_picker_calendar').get_date() + date_string = '{}-{:02d}-{:02d}'.format(date.year, date.month +1, date.day) + entry.set_text(date_string) + + @staticmethod + def do_set_today(cal: 'Gtk.Calendar'): + """Set date of a Gtk Calendar to today + :param cal: Gtk.Calendar + """ + cal.select_month = datetime.date.today().month + cal.select_day = datetime.date.today().day diff --git a/dsst/dsst_gtk3/handlers/handlers.py b/dsst/dsst_gtk3/handlers/handlers.py index 8856cce..73c93b4 100644 --- a/dsst/dsst_gtk3/handlers/handlers.py +++ b/dsst/dsst_gtk3/handlers/handlers.py @@ -6,7 +6,6 @@ from dsst_gtk3.handlers.base_data_handlers import BaseDataHandlers from dsst_gtk3.handlers.dialog_handlers import DialogHandlers from dsst_gtk3.handlers.death_handlers import DeathHandlers from dsst_gtk3.handlers.victory_handlers import VictoryHandlers -from dsst_sql import sql, sql_func class Handlers(SeasonHandlers, BaseDataHandlers, DialogHandlers, DeathHandlers, VictoryHandlers): @@ -28,12 +27,10 @@ class Handlers(SeasonHandlers, BaseDataHandlers, DialogHandlers, DeathHandlers, """ Signal will be sent when app should close :param _: Arguments to the delete event """ - sql.db.close() Gtk.main_quit() # DEBUG Functions ################################################################################################## @staticmethod def do_delete_database(*_): - sql_func.drop_tables() - sql_func.create_tables() + pass diff --git a/dsst/dsst_gtk3/handlers/season_handlers.py b/dsst/dsst_gtk3/handlers/season_handlers.py index 1d667f7..c213bb1 100644 --- a/dsst/dsst_gtk3/handlers/season_handlers.py +++ b/dsst/dsst_gtk3/handlers/season_handlers.py @@ -1,5 +1,4 @@ -from dsst_sql import sql -from dsst_gtk3 import dialogs, gtk_ui +from dsst_gtk3 import dialogs, gtk_ui, reload class SeasonHandlers: @@ -8,23 +7,27 @@ class SeasonHandlers: self.app = app def do_add_season(self, *_): - name = dialogs.enter_string_dialog(self.app.ui, 'Name for the new Season') - if name: - sql.Season.create(game_name=name, number=1) - self.app.reload() + season = dialogs.edit_season(self.app.ui) + if season: + self.app.update_season(season) + self.app.full_reload() def do_season_selected(self, *_): - self.app.reload() + self.app.episodes.valid = False + self.app.season_stats.valid = False + self.app.full_reload() def do_add_episode(self, *_): season_id = self.app.get_selected_season_id() if not season_id: return - dialogs.show_episode_dialog(self.app.ui, 'Create new Episode', season_id) - self.app.reload() + ep = dialogs.edit_episode(self.app, season_id) + if ep: + self.app.update_episode(ep) + self.app.full_reload() def on_selected_episode_changed(self, *_): - self.app.reload() + reload.reload_episode_stats(self.app) def on_episode_double_click(self, *_): self.app.ui.get_object('stats_notebook').set_current_page(1) diff --git a/dsst/dsst_gtk3/handlers/victory_handlers.py b/dsst/dsst_gtk3/handlers/victory_handlers.py index d130a34..3b47122 100644 --- a/dsst/dsst_gtk3/handlers/victory_handlers.py +++ b/dsst/dsst_gtk3/handlers/victory_handlers.py @@ -11,6 +11,6 @@ class VictoryHandlers: ep_id = self.app.get_selected_episode_id() if not ep_id: return - result = dialogs.show_edit_victory_dialog(self.app.ui, ep_id) - if result == Gtk.ResponseType.OK: - self.app.reload() + victory = dialogs.create_victory(self.app) + if victory: + self.app.save_victory(victory) diff --git a/dsst/dsst_gtk3/reload.py b/dsst/dsst_gtk3/reload.py index 6d1bca8..bc1d386 100644 --- a/dsst/dsst_gtk3/reload.py +++ b/dsst/dsst_gtk3/reload.py @@ -1,116 +1,123 @@ from collections import Counter from gi.repository import Gtk -from dsst_gtk3 import gtk_ui -from dsst_sql import sql, sql_func -from dsst_gtk3 import util +from dsst_gtk3 import util, gtk_ui -def reload_base_data(builder: Gtk.Builder, app: 'gtk_ui.GtkUi'): +def reload_base_data(app: 'gtk_ui.GtkUi',): """Reload function for all base data witch is not dependant on a selected season or episode :param app: GtkUi instance :param builder: Gtk.Builder with loaded UI """ # Rebuild all players store - builder.get_object('all_players_store').clear() - for player in sql.Player.select(): - builder.get_object('all_players_store').append([player.id, player.name, player.hex_id]) + app.ui.get_object('all_players_store').clear() + for player in app.players.data: + app.ui.get_object('all_players_store').append([player.id, player.name, player.hex_id]) # Rebuild drink store - builder.get_object('drink_store').clear() - for drink in sql.Drink.select(): - builder.get_object('drink_store').append([drink.id, drink.name, '{:.2f}%'.format(drink.vol)]) + app.ui.get_object('drink_store').clear() + for drink in app.drinks.data: + app.ui.get_object('drink_store').append([drink.id, drink.name, '{:.2f}%'.format(drink.vol)]) # Rebuild seasons store - combo = builder.get_object('season_combo_box') # type: Gtk.ComboBox + combo = app.ui.get_object('season_combo_box') # type: Gtk.ComboBox active = combo.get_active() with util.block_handler(combo, app.handlers.do_season_selected): - store = builder.get_object('seasons_store') + store = app.ui.get_object('seasons_store') store.clear() - for season in sql.Season.select().order_by(sql.Season.number): + for season in app.seasons.data: store.append([season.id, season.game_name]) combo.set_active(active) -def reload_episodes(builder: Gtk.Builder, app: 'gtk_ui.GtkUi', season_id: int): +def reload_episodes(app: 'gtk_ui.GtkUi'): """Reload all data that is dependant on a selected season :param app: GtkUi instance :param builder: Gtk.Builder with loaded UI - :param season_id: ID of the season for witch to load data """ # Rebuild episodes store - selection = builder.get_object('episodes_tree_view').get_selection() + if not app.get_selected_season_id(): return + selection = app.ui.get_object('episodes_tree_view').get_selection() with util.block_handler(selection, app.handlers.on_selected_episode_changed): model, selected_paths = selection.get_selected_rows() model.clear() - for episode in sql_func.get_episodes_for_season(season_id): + for episode in app.episodes.data: model.append([episode.id, episode.name, str(episode.date), episode.number]) if selected_paths: selection.select_path(selected_paths[0]) -def reload_season_stats(builder: Gtk.Builder, app: 'gtk_ui.GtkUi', season_id: int): +def reload_season_stats(app: 'gtk_ui.GtkUi'): """Load statistic data for selected season - :param builder: Gtk.Builder with loaded UI :param app: GtkUi instance - :param season_id: ID of the season for witch to load data """ - player_stats = {} - for episode in sql_func.get_episodes_for_season(season_id): - for player in episode.players: - player_stats[player.name] = [sql_func.get_player_deaths_for_season(season_id, player.id), - sql_func.get_player_victories_for_season(season_id, player.id)] - store = builder.get_object('player_season_store') + if not app.season_stats.valid: return + season_stats = app.season_stats.data + # Load player kill/death data + store = app.ui.get_object('player_season_store') store.clear() - for name, stats in player_stats.items(): - store.append([name, stats[0], stats[1]]) + for player_name, kills, deaths in season_stats.player_kd: + store.append([player_name, deaths, kills]) + # Load enemy stats for season - season = sql.Season.get(sql.Season.id == season_id) - enemy_stats = { - enemy.name: [False, len(sql.Death.select().where(sql.Death.enemy == enemy)), enemy.id] - for enemy in season.enemies} - store = builder.get_object('enemy_season_store') + store = app.ui.get_object('enemy_season_store') store.clear() - for name, stats in enemy_stats.items(): - store.append([name, stats[0], stats[1], stats[2]]) + for enemy_id, enemy_name, deaths, defeated, boss in season_stats.enemies: + store.append([enemy_name, defeated, deaths, boss, enemy_id]) -def reload_episode_stats(builder: Gtk.Builder, app: 'gtk_ui.GtkUi', episode_id: int): +def reload_episode_stats(app: 'gtk_ui.GtkUi'): """Reload all data that is dependant on a selected episode - :param builder: builder: Gtk.Builder with loaded UI :param app: app: GtkUi instance - :param episode_id: ID of the episode for witch to load data """ - episode = sql.Episode.get(sql.Episode.id == episode_id) - store = builder.get_object('episode_players_store') + ep_id = app.get_selected_episode_id() + if not app.episodes.valid or not ep_id: return + episode = [ep for ep in app.episodes.data if ep.id == ep_id][0] + store = app.ui.get_object('episode_players_store') store.clear() for player in episode.players: store.append([player.id, player.name, player.hex_id]) # Reload death store for notebook view - store = builder.get_object('episode_deaths_store') + store = app.ui.get_object('episode_deaths_store') store.clear() for death in episode.deaths: penalties = [x.drink.name for x in death.penalties] - penalties = [f'{number}x {drink}' for drink, number in Counter(penalties).items()] + penalties = ['{}x {}'.format(number, drink) for drink, number in Counter(penalties).items()] penalty_string = ', '.join(penalties) - store.append([death.id, death.player.name, death.enemy.name, penalty_string]) + time_string = '{:02d}:{:02d}'.format(death.time.hour, death.time.minute) + store.append([death.id, death.player.name, death.enemy.name, penalty_string, time_string]) # Reload victory store for notebook view - store = builder.get_object('episode_victories_store') + store = app.ui.get_object('episode_victories_store') store.clear() for victory in episode.victories: - store.append([victory.id, victory.player.name, victory.enemy.name, victory.info]) + time_string = '{:02d}:{:02d}'.format(victory.time.hour, victory.time.minute) + store.append([victory.id, victory.player.name, victory.enemy.name, victory.info, time_string]) # Stat grid - builder.get_object('ep_stat_title').set_text('Stats for episode {}\n{}'.format(episode.number, episode.name)) - builder.get_object('ep_death_count_label').set_text(str(len(episode.deaths))) + app.ui.get_object('ep_stat_title').set_text('Stats for episode {}\n{}'.format(episode.number, episode.name)) + app.ui.get_object('ep_death_count_label').set_text(str(len(episode.deaths))) drink_count = sum(len(death.penalties) for death in episode.deaths) - builder.get_object('ep_drinks_label').set_text(str(drink_count)) - builder.get_object('ep_player_drinks_label').set_text(str(len(episode.deaths))) + app.ui.get_object('ep_drinks_label').set_text(str(drink_count)) + app.ui.get_object('ep_player_drinks_label').set_text(str(len(episode.deaths))) dl_booze = sum(len(death.penalties) * death.penalties[0].size for death in episode.deaths) l_booze = round(dl_booze / 10, 2) - builder.get_object('ep_booze_label').set_text('{}l'.format(l_booze)) + app.ui.get_object('ep_booze_label').set_text('{}l'.format(l_booze)) dl_booze = sum(len(death.penalties) * death.penalties[0].size for death in episode.deaths) ml_booze = round(dl_booze * 10, 0) - builder.get_object('ep_player_booze_label').set_text('{}ml'.format(ml_booze)) + app.ui.get_object('ep_player_booze_label').set_text('{}ml'.format(ml_booze)) enemy_list = [death.enemy.name for death in episode.deaths] sorted_list = Counter(enemy_list).most_common(1) if sorted_list: enemy_name, deaths = sorted_list[0] - builder.get_object('ep_enemy_name_label').set_text(f'{enemy_name} ({deaths} Deaths)') \ No newline at end of file + app.ui.get_object('ep_enemy_name_label').set_text('{} ({} Deaths)'.format(enemy_name, deaths)) + + +def fill_list_store(store: Gtk.ListStore, models: list): + store.clear() + for model in models: + pass + + +def rebuild_view_data(app: 'gtk_ui.GtkUi'): + reload_base_data(app) + reload_episodes(app) + reload_episode_stats(app) + reload_season_stats(app) + diff --git a/dsst/dsst_gtk3/resources/glade/dialogs.glade b/dsst/dsst_gtk3/resources/glade/dialogs.glade index 8b9b6ad..108fb46 100644 --- a/dsst/dsst_gtk3/resources/glade/dialogs.glade +++ b/dsst/dsst_gtk3/resources/glade/dialogs.glade @@ -1,7 +1,7 @@ - + - + False False diff --git a/dsst/dsst_gtk3/resources/glade/window.glade b/dsst/dsst_gtk3/resources/glade/window.glade index 9312066..c058222 100644 --- a/dsst/dsst_gtk3/resources/glade/window.glade +++ b/dsst/dsst_gtk3/resources/glade/window.glade @@ -1,7 +1,7 @@ - + - + @@ -12,174 +12,6 @@ - - False - Manage Players - False - True - 300 - dialog - False - - - False - vertical - 4 - - - False - end - - - - - - gtk-ok - 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 - in - - - True - True - True - all_players_store - 0 - - - - - - Name - - - True - - - - 1 - - - - - - - Hex ID - - - True - - - - 2 - - - - - - - - - True - True - 2 - - - - - True - True - 1 - - - - - - okButtonRename2 - - - - - @@ -190,174 +22,6 @@ - - False - Manage Drinks - False - True - 300 - dialog - False - - - False - vertical - 4 - - - False - end - - - - - - gtk-ok - True - True - True - True - - - True - True - 1 - - - - - False - False - 0 - - - - - True - False - vertical - 5 - - - True - False - 5 - - - True - False - 5 - 5 - Add Drink - - - False - True - 0 - - - - - True - True - - - - False - True - 1 - - - - - False - True - 0 - - - - - True - False - 5 - 5 - All Drinks - 0 - - - False - True - 1 - - - - - True - True - in - - - True - True - True - drink_store - 0 - - - - - - Name - - - True - - - - 1 - - - - - - - Vol. - - - True - - - - 2 - - - - - - - - - True - True - 2 - - - - - True - True - 1 - - - - - - okButtonRename3 - - - - - @@ -366,161 +30,12 @@ + + - - False - Manage Enemies For This Season - False - True - 300 - dialog - False - - - False - vertical - 4 - - - False - end - - - - - - gtk-ok - 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 - in - - - True - True - True - enemy_season_store - 0 - - - - - - Name - - - - 0 - - - - - - - - - True - True - 2 - - - - - True - True - 1 - - - - - - okButtonRename1 - - - - - 1000000 1 @@ -536,6 +51,8 @@ + + @@ -548,510 +65,6 @@ - - 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 - 5 - - - True - False - vertical - 5 - - - True - False - - - True - False - Episode No. - - - False - True - 0 - - - - - True - True - 0 - digits - ep_number_ajustment - 1 - True - True - - - False - True - end - 1 - - - - - False - True - 0 - - - - - True - False - - - True - False - Episode Name - - - False - True - 0 - - - - - True - True - - - False - True - end - 1 - - - - - False - True - 1 - - - - - True - False - - - True - False - Player - - - False - True - 0 - - - - - True - False - all_players_store - 1 - 1 - - - - 1 - - - - - True - True - 1 - - - - - Add - True - True - True - - - - False - True - end - 2 - - - - - False - True - 2 - - - - - True - True - in - - - True - True - True - episode_players_store - 0 - - - - - - Name - - - - 1 - - - - - - - Hex ID - - - - 2 - - - - - - - - - True - True - 3 - - - - - False - True - 0 - - - - - True - False - vertical - - - True - False - 5 - 5 - Episode Date - 0 - - - False - True - 0 - - - - - True - True - 2018 - 1 - 23 - - - False - True - 1 - - - - - False - True - 1 - - - - - False - True - 1 - - - - - - button1 - button2 - - - - - - - False - Edit Victory Event - 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 - Enemy - - - False - True - 0 - - - - - True - False - enemy_season_store - - - - 0 - - - - - True - True - end - 1 - - - - - False - True - 0 - - - - - True - False - - - True - False - 5 - 5 - Player - - - False - True - 0 - - - - - True - False - episode_players_store - - - - 1 - - - - - True - True - end - 1 - - - - - False - True - 1 - - - - - True - False - - - True - False - Comment - - - False - True - 0 - - - - - True - True - - - True - True - end - 1 - - - - - False - True - 2 - - - - - True - True - 1 - - - - - - okButtonRename5 - cancelButtonRename1 - - - - - @@ -1062,6 +75,8 @@ + + @@ -1076,6 +91,16 @@ + + 24 + 1 + 1 + + + 60 + 1 + 1 + @@ -1088,291 +113,6 @@ - - False - Edit Death Event - 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 - Enemy - - - False - True - 0 - - - - - True - False - enemy_season_store - - - - 0 - - - - - True - True - end - 1 - - - - - False - True - 0 - - - - - True - False - - - True - False - 5 - 5 - 5 - 5 - Player - - - False - True - 0 - - - - - True - False - episode_players_store - - - - 1 - - - - - True - True - end - 1 - - - - - False - True - 1 - - - - - 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 - player_penalties_store - 0 - - - - - - Player - - - - 1 - - - - - - - Penalty - - - True - False - drink_store - 1 - - - - 2 - - - - - - - True - True - 4 - - - - - True - True - 1 - - - - - - okButtonRename4 - cancelButtonRename4 - - - - - @@ -1426,6 +166,15 @@ + + + True + False + Add Episode + True + + + True @@ -1438,7 +187,7 @@ False Manage Enemies True - + @@ -1510,7 +259,7 @@ False Manage Players True - + @@ -1519,7 +268,7 @@ False Manage Drinks True - + @@ -1591,23 +340,7 @@ - - New - True - True - True - 5 - 5 - 5 - 5 - - - - False - True - end - 1 - + @@ -1665,23 +398,7 @@ - - New - True - True - True - 5 - 5 - 5 - 5 - - - - False - True - end - 1 - + @@ -1700,6 +417,7 @@ True True episodes_store + False 0 @@ -2076,6 +794,17 @@ + + + Time + + + + 4 + + + + Player @@ -2145,6 +874,17 @@ + + + Time + + + + 4 + + + + Player @@ -2264,6 +1004,7 @@ Name + True True True 0 @@ -2345,7 +1086,9 @@ + True Name + True True True 0 @@ -2360,6 +1103,7 @@ + True Deaths True True @@ -2367,12 +1111,28 @@ - 1 2 + + + 40 + Boss + True + True + 3 + + + checkmark + + + 3 + + + + @@ -2417,6 +1177,7 @@ Name + True @@ -2484,7 +1245,7 @@ False True - 2 + 1 @@ -2498,7 +1259,7 @@ False True - 3 + 2 @@ -2510,7 +1271,7 @@ False True - 4 + 3 @@ -2521,6 +1282,18 @@ + + False + True + 4 + + + + False True @@ -2540,4 +1313,1830 @@ + + False + Pick Date + False + 300 + dialog + False + main_window + + + False + vertical + 2 + + + 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 + + + True + False + Pick Date + + + False + True + 0 + + + + + Today + True + True + True + + + + False + True + end + 1 + + + + + False + True + 0 + + + + + True + True + 2018 + 2 + 9 + + + False + True + 2 + + + + + + button3 + button6 + + + + + + + False + Edit Episode + False + True + dialog + False + main_window + + + 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 + 5 + + + True + False + vertical + 5 + + + True + False + + + True + False + Episode No. + + + False + True + 0 + + + + + True + True + 0 + digits + ep_number_ajustment + 1 + True + True + + + False + True + end + 1 + + + + + False + True + 0 + + + + + True + False + + + True + False + Episode Name + + + False + True + 0 + + + + + True + True + + + False + True + end + 1 + + + + + False + True + 1 + + + + + True + False + + + True + False + Player + + + False + True + 0 + + + + + True + False + all_players_store + 1 + 1 + + + + 1 + + + + + True + True + 1 + + + + + Add + True + True + True + + + + False + True + end + 2 + + + + + False + True + 2 + + + + + True + True + in + + + True + True + True + episode_players_store + 0 + + + + + + Name + + + + 1 + + + + + + + Hex ID + + + + 2 + + + + + + + + + True + True + 3 + + + + + False + True + 0 + + + + + True + False + vertical + + + True + False + 5 + 5 + Episode Date + 0 + + + False + True + 0 + + + + + True + True + 2018 + 1 + 23 + + + False + True + 1 + + + + + False + True + 1 + + + + + False + True + 1 + + + + + + button1 + button2 + + + + + + + False + Edit Season + False + dialog + False + main_window + main_window + + + False + vertical + 2 + + + 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 + + + True + False + + + True + False + Season Number: + + + False + True + 0 + + + + + True + True + digits + ep_number_ajustment + True + + + True + True + end + 1 + + + + + False + True + 0 + + + + + True + False + + + True + False + Game Name: + + + False + True + 0 + + + + + True + True + + + True + True + end + 1 + + + + + False + True + 1 + + + + + True + False + + + True + False + Start Date: + + + False + True + 0 + + + + + True + False + gtk-edit + + + + True + True + end + 1 + + + + + False + True + 2 + + + + + True + False + + + True + False + End Date: + + + False + True + 0 + + + + + True + False + gtk-edit + + + + True + True + end + 1 + + + + + False + True + 3 + + + + + True + True + 1 + + + + + + button4 + button5 + + + + + + + False + Create Victory Event + False + True + dialog + False + main_window + + + 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 + Time + + + False + True + 0 + + + + + True + False + + + True + True + 2 + 0 + number + hour_adjustment + True + if-valid + + + False + True + 0 + + + + + True + False + : + + + False + True + 1 + + + + + True + True + 2 + 0 + number + minute_adjustment + True + if-valid + + + False + True + 2 + + + + + True + True + end + 1 + + + + + False + True + 1 + + + + + True + False + 5 + + + True + False + 5 + 5 + Enemy + + + False + True + 0 + + + + + True + False + enemy_season_store + + + + 0 + + + + + True + True + end + 1 + + + + + False + True + 1 + + + + + True + False + + + True + False + 5 + 5 + Player + + + False + True + 0 + + + + + True + False + episode_players_store + + + + 1 + + + + + True + True + end + 1 + + + + + False + True + 2 + + + + + True + False + + + True + False + Comment + + + False + True + 0 + + + + + True + True + + + True + True + end + 1 + + + + + False + True + 3 + + + + + True + True + 1 + + + + + + okButtonRename5 + cancelButtonRename1 + + + + + + + False + Manage Drinks + False + True + 300 + dialog + False + main_window + + + False + vertical + 4 + + + False + end + + + + + + gtk-ok + True + True + True + True + + + True + True + 1 + + + + + False + False + 0 + + + + + True + False + vertical + 5 + + + True + False + 5 + + + True + False + 5 + 5 + Add Drink + + + False + True + 0 + + + + + True + True + + + + False + True + 1 + + + + + False + True + 0 + + + + + True + False + 5 + 5 + All Drinks + 0 + + + False + True + 1 + + + + + True + True + in + + + True + True + True + drink_store + 0 + + + + + + Name + True + + + True + + + + 1 + + + + + + + Vol. + + + True + + + + 2 + + + + + + + + + True + True + 2 + + + + + True + True + 1 + + + + + + okButtonRename3 + + + + + + + False + Manage Enemies For This Season + False + True + 300 + dialog + False + main_window + + + False + vertical + 4 + + + False + end + + + + + + gtk-ok + 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 + + + + + Optional + True + True + False + True + + + False + True + 2 + + + + + False + True + 0 + + + + + True + False + 5 + 5 + All Enemies + 0 + + + False + True + 1 + + + + + True + True + in + + + True + True + True + enemy_season_store + 0 + + + + + + Name + True + + + True + + + + 0 + + + + + + + Boss + + + + + + 3 + + + + + + + + + True + True + 2 + + + + + True + True + 1 + + + + + + okButtonRename1 + + + + + + + False + Manage Players + False + True + 300 + dialog + False + main_window + + + False + vertical + 4 + + + False + end + + + + + + gtk-ok + 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 + in + + + True + True + True + all_players_store + 0 + + + + + + Name + + + True + + + + 1 + + + + + + + Hex ID + + + True + + + + 2 + + + + + + + + + True + True + 2 + + + + + True + True + 1 + + + + + + okButtonRename2 + + + + + + + 100 + 0.20000000000000001 + 0.20000000000000001 + + + False + Edit Death Event + False + True + dialog + False + main_window + + + 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 + Time + + + False + True + 0 + + + + + True + False + + + True + True + 2 + number + hour_adjustment + True + if-valid + + + False + True + 0 + + + + + True + False + : + + + False + True + 1 + + + + + True + True + 2 + number + minute_adjustment + True + if-valid + + + False + True + 2 + + + + + False + True + end + 1 + + + + + False + True + 0 + + + + + True + False + 5 + + + True + False + 5 + 5 + Enemy + + + False + True + 0 + + + + + True + False + enemy_season_store + + + + 0 + + + + + True + True + end + 1 + + + + + False + True + 1 + + + + + True + False + + + True + False + 5 + 5 + 5 + 5 + Player + + + False + True + 0 + + + + + True + False + episode_players_store + + + + 1 + + + + + True + True + end + 1 + + + + + False + True + 2 + + + + + True + False + + + True + False + 5 + 5 + 5 + 5 + Drink Size + + + False + True + 0 + + + + + True + True + shot_size_adjustment + 1 + True + if-valid + 0.20000000000000001 + + + False + True + end + 1 + + + + + False + True + 3 + + + + + True + False + + + True + False + Comment + + + False + True + 0 + + + + + True + True + + + True + True + end + 1 + + + + + False + True + 4 + + + + + 100 + True + True + True + player_penalties_store + 0 + + + + + + Player + + + + 1 + + + + + + + Penalty + + + True + False + drink_store + 1 + + + + 2 + + + + + + + True + True + 5 + + + + + True + True + 1 + + + + + + okButtonRename4 + cancelButtonRename4 + + + + + diff --git a/dsst/dsst_gtk3/resources/images/dd.png b/dsst/dsst_gtk3/resources/images/dd.png new file mode 100644 index 0000000..b455b2e Binary files /dev/null and b/dsst/dsst_gtk3/resources/images/dd.png differ diff --git a/dsst/dsst_gtk3/util.py b/dsst/dsst_gtk3/util.py index 391525f..a535d99 100644 --- a/dsst/dsst_gtk3/util.py +++ b/dsst/dsst_gtk3/util.py @@ -4,23 +4,38 @@ This modules contains general utilities for the GTK application to use. import json import os from contextlib import contextmanager -from gi.repository import Gtk +from gi.repository import Gtk, GdkPixbuf from typing import Callable +from dsst_gtk3 import gtk_ui from zipfile import ZipFile CONFIG_PATH = os.path.join(os.path.expanduser('~'), '.config', 'dsst', 'config.json') DEFAULT_CONFIG = { 'auto_connect': False, - 'sql_connections': [{ + 'servers': [{ 'host': 'localhost', - 'port': 3306, - 'db_name': 'dsst', - 'user': 'dsst', - 'password': 'dsst'} + 'port': 12345, + 'buffer_size': 1024, + 'auth_token': ''} ] } +class Cache: + def __init__(self, data={}, valid=False): + self._data = data + self.valid = valid + + @property + def data(self): + return self._data + + @data.setter + def data(self, value): + self._data = value + self.valid = True + + @contextmanager def block_handler(widget: 'Gtk.Widget', handler_func: Callable): """Run an operation while a signal handler for a widget is blocked @@ -32,6 +47,21 @@ def block_handler(widget: 'Gtk.Widget', handler_func: Callable): widget.handler_unblock_by_func(handler_func) +@contextmanager +def network_operation(app: 'gtk_ui.GtkUi'): + """Run operation in try/except block and display exception in a dialog + :param app: Reference to main Gtk Application + """ + app.ui.get_object('status_bar').push(0, 'Connecting to server') + try: + yield + except Exception as e: + print(e) + app.ui.get_object('status_bar').push(0, str(e)) + else: + app.ui.get_object('status_bar').push(0, '') + + def get_combo_value(combo, index: int): """ Retrieve the selected value of a combo box at the selected index in the model :param combo: Any Gtk Widget that supports 'get_active_iter()' @@ -59,7 +89,7 @@ def get_index_of_combo_model(widget, column: int, value: int): 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: + with open(full_path, 'r', encoding='utf8') as file: return file.read() @@ -77,10 +107,35 @@ def load_ui_resource_string(resource_path: list) -> str: if os.path.isdir(os.path.dirname(__file__)): return load_ui_resource_from_file(resource_path) else: - return load_ui_resource_from_archive(resource_path) +def load_image_resource_file(resource_path: list, width: int, height: int) -> GdkPixbuf: + project_base_dir = os.path.dirname(os.path.dirname(__file__)) + full_path = os.path.join(project_base_dir, *resource_path) + return GdkPixbuf.Pixbuf.new_from_file_at_scale(full_path, width=width, height=height, preserve_aspect_ratio=False) + + +def load_image_resource_archive(resource_path: list, width: int, height: int) -> GdkPixbuf: + resource_path = os.path.join(*resource_path) + zip_path = os.path.dirname(os.path.dirname(__file__)) + with ZipFile(zip_path, 'r') as archive: + with archive.open(resource_path) as data: + loader = GdkPixbuf.PixbufLoader() + loader.write(data.read()) + pixbuf = loader.get_pixbuf() # type: GdkPixbuf.Pixbuf + pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR) + loader.close() + return pixbuf + + +def load_image_resource(resource_path: list, width: int, height: int) -> GdkPixbuf: + if os.path.isdir(os.path.dirname(__file__)): + return load_image_resource_file(resource_path, width, height) + else: + return load_image_resource_archive(resource_path, width, height) + + def load_config(config_path: str) -> dict: with open(config_path) as config_file: return json.load(config_file) diff --git a/dsst/dsst_server/__init__.py b/dsst/dsst_server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dsst/dsst_server/__main__.py b/dsst/dsst_server/__main__.py new file mode 100644 index 0000000..267e28f --- /dev/null +++ b/dsst/dsst_server/__main__.py @@ -0,0 +1,11 @@ +import os.path +import sys + +# Add current directory to python path +path = os.path.realpath(os.path.abspath(__file__)) +sys.path.insert(0, os.path.dirname(os.path.dirname(path))) + +from dsst_server import server + +if __name__ == '__main__': + server.main() diff --git a/dsst/dsst_server/data_access/__init__.py b/dsst/dsst_server/data_access/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dsst/dsst_server/data_access/mapping.py b/dsst/dsst_server/data_access/mapping.py new file mode 100644 index 0000000..ceeb1dc --- /dev/null +++ b/dsst/dsst_server/data_access/mapping.py @@ -0,0 +1,70 @@ +from dsst_server.data_access import sql +from common import models + + +def map_base_fields(cls, db_model): + """Automatically map fields of db models to common models + :param cls: common.models class to create + :param db_model: database model from which to map + :return: An common.models object + """ + model = cls() + attrs = [attr for attr in db_model._meta.fields] + for attr in attrs: + db_attr = getattr(db_model, attr) + # Check if the attribute is an relation to another db model + # In that case just take its id + if hasattr(db_attr, 'id'): + setattr(model, attr, getattr(db_attr, 'id')) + else: + setattr(model, attr, getattr(db_model, attr)) + return model + + +def db_to_drink(drink: 'sql.Drink'): + return map_base_fields(models.Drink, drink) + + +def db_to_enemy(enemy: 'sql.Enemy'): + return map_base_fields(models.Enemy, enemy) + + +def db_to_player(player: 'sql.Player'): + return map_base_fields(models.Player, player) + + +def db_to_penalty(penalty: 'sql.Penalty'): + model = map_base_fields(models.Penalty, penalty) + model.drink = db_to_drink(penalty.drink) + model.player = db_to_player(penalty.player) + return model + + +def db_to_death(death: 'sql.Death'): + model = map_base_fields(models.Death, death) + model.player = db_to_player(death.player) + model.enemy = db_to_enemy(death.enemy) + model.penalties = [db_to_penalty(penalty) for penalty in death.penalties] + return model + + +def db_to_victory(victory: 'sql.Victory'): + model = map_base_fields(models.Victory, victory) + model.player = db_to_player(victory.player) + model.enemy = db_to_enemy(victory.enemy) + return model + + +def db_to_episode(episode: 'sql.Episode'): + model = map_base_fields(models.Episode, episode) + model.players = [db_to_player(player) for player in episode.players] + model.deaths = [db_to_death(death) for death in episode.deaths] + model.victories = [db_to_victory(victory) for victory in episode.victories] + return model + + +def db_to_season(season: 'sql.Season'): + model = map_base_fields(models.Season, season) + model.enemies = [db_to_enemy(enemy) for enemy in season.enemies] + model.episodes = [db_to_episode(ep) for ep in season.episodes] + return model diff --git a/dsst/dsst_sql/sql.py b/dsst/dsst_server/data_access/sql.py similarity index 88% rename from dsst/dsst_sql/sql.py rename to dsst/dsst_server/data_access/sql.py index 16052c2..90949d9 100644 --- a/dsst/dsst_sql/sql.py +++ b/dsst/dsst_server/data_access/sql.py @@ -5,8 +5,14 @@ Example: from sql import Episode query = Episode.select().where(Episode.name == 'MyName') """ +import sys +import datetime -from peewee import * +try: + from peewee import * +except ImportError: + print('peewee package not installed') + sys.exit(0) db = MySQLDatabase(None) @@ -56,7 +62,7 @@ class Drink(Model): class Enemy(Model): id = AutoField() name = CharField() - optional = BooleanField() + boss = BooleanField() season = ForeignKeyField(Season, backref='enemies') class Meta: @@ -66,6 +72,7 @@ class Enemy(Model): class Death(Model): id = AutoField() info = CharField(null=True) + time = TimeField(default=datetime.time(0, 0)) player = ForeignKeyField(Player) enemy = ForeignKeyField(Enemy) episode = ForeignKeyField(Episode, backref='deaths') @@ -88,6 +95,7 @@ class Penalty(Model): class Victory(Model): id = AutoField() info = CharField(null=True) + time = TimeField(default=datetime.time(0, 0)) player = ForeignKeyField(Player) enemy = ForeignKeyField(Enemy) episode = ForeignKeyField(Episode, backref='victories') diff --git a/dsst/dsst_sql/sql_func.py b/dsst/dsst_server/data_access/sql_func.py similarity index 57% rename from dsst/dsst_sql/sql_func.py rename to dsst/dsst_server/data_access/sql_func.py index 11139ee..0166bdc 100644 --- a/dsst/dsst_sql/sql_func.py +++ b/dsst/dsst_server/data_access/sql_func.py @@ -1,7 +1,7 @@ """ This module contains shorthand functions for common queries to ease access from the UI """ -from dsst_sql.sql import * +from dsst_server.data_access import sql def get_episodes_for_season(season_id: int) -> list: @@ -10,8 +10,8 @@ def get_episodes_for_season(season_id: int) -> list: :return: List of sql.Episode or empty list """ try: - return list(Season.get(Season.id == season_id).episodes) - except Episode.DoesNotExist: + return list(sql.Season.get(sql.Season.id == season_id).episodes) + except sql.Episode.DoesNotExist: return [] @@ -22,7 +22,7 @@ def get_player_deaths_for_season(season_id: int, player_id: int) -> int: :return: Number of deaths of the player in the season """ deaths = 0 - for episode in list(Season.get(Season.id == season_id).episodes): + for episode in list(sql.Season.get(sql.Season.id == season_id).episodes): deaths = deaths + len([death for death in list(episode.deaths) if death.player.id == player_id]) return deaths @@ -34,19 +34,33 @@ def get_player_victories_for_season(season_id: int, player_id: int) -> int: :return: Number of all victories of the player in the season """ victories = 0 - for episode in list(Season.get(Season.id == season_id).episodes): + for episode in list(sql.Season.get(sql.Season.id == season_id).episodes): victories = victories + len([vic for vic in list(episode.victories) if vic.player.id == player_id]) return victories +def players_for_season(season_id: int) -> set: + season_eps = list(sql.Season.get(sql.Season.id == season_id).episodes) + players = set() + for ep in season_eps: + players.update([player for player in ep.players]) + return players + + +def enemy_attempts(enemy_id: int) -> int: + return sql.Death.select().where(sql.Death.enemy == enemy_id).count() + + def create_tables(): """Create all database tables""" - models = [Season, Episode, Player, Drink, Enemy, Death, Victory, Penalty, Episode.players.get_through_model()] + models = [sql.Season, sql.Episode, sql.Player, sql.Drink, sql.Enemy, sql.Death, sql.Victory, sql.Penalty, + sql.Episode.players.get_through_model()] for model in models: model.create_table() def drop_tables(): """Drop all data in database""" - models = [Season, Episode, Player, Drink, Enemy, Death, Victory, Penalty, Episode.players.get_through_model()] - db.drop_tables(models) \ No newline at end of file + models = [sql.Season, sql.Episode, sql.Player, sql.Drink, sql.Enemy, sql.Death, sql.Victory, sql.Penalty, + sql.Episode.players.get_through_model()] + sql.db.drop_tables(models) \ No newline at end of file diff --git a/dsst/dsst_server/func_proxy.py b/dsst/dsst_server/func_proxy.py new file mode 100644 index 0000000..326f1a2 --- /dev/null +++ b/dsst/dsst_server/func_proxy.py @@ -0,0 +1,6 @@ +from dsst_server.func_write import WriteFunctions +from dsst_server.func_read import ReadFunctions + + +class FunctionProxy(WriteFunctions, ReadFunctions): + pass diff --git a/dsst/dsst_server/func_read.py b/dsst/dsst_server/func_read.py new file mode 100644 index 0000000..40afa44 --- /dev/null +++ b/dsst/dsst_server/func_read.py @@ -0,0 +1,52 @@ +from dsst_server.data_access import sql, sql_func, mapping +from common import models +from playhouse import shortcuts + + +class ReadFunctions: + @staticmethod + def load_db_meta(*_): + return sql.db.database + + @staticmethod + def load_seasons(*_): + return [mapping.db_to_season(season) for season in sql.Season.select()] + + @staticmethod + def load_seasons_all(*_): + return [shortcuts.model_to_dict(season, backrefs=True, max_depth=2) for season in sql.Season.select()] + + @staticmethod + def load_episodes(season_id, *_): + if not season_id: + raise Exception('Exception: Missing argument (season_id)') + return [mapping.db_to_episode(ep) for ep in sql.Season.get(sql.Season.id == season_id).episodes] + + @staticmethod + def load_players(*_): + return [mapping.db_to_player(player) for player in sql.Player.select()] + + @staticmethod + def load_enemies(season_id, *_): + pass + + @staticmethod + def load_drinks(*_): + return [mapping.db_to_drink(drink) for drink in sql.Drink.select()] + + @staticmethod + def load_season_stats(season_id, *_): + season = sql.Season.get(sql.Season.id == season_id) + players = sql_func.players_for_season(season_id) + model = models.SeasonStats() + model.player_kd = [(player.name, + sql_func.get_player_victories_for_season(season_id, player.id), + sql_func.get_player_deaths_for_season(season_id, player.id)) + for player in players] + model.enemies = [(enemy.id, + enemy.name, + sql_func.enemy_attempts(enemy.id), + sql.Victory.select().where(sql.Victory.enemy == enemy.id).exists(), + enemy.boss) + for enemy in season.enemies] + return model diff --git a/dsst/dsst_server/func_write.py b/dsst/dsst_server/func_write.py new file mode 100644 index 0000000..5aebaa4 --- /dev/null +++ b/dsst/dsst_server/func_write.py @@ -0,0 +1,78 @@ +from common import models +from dsst_server.data_access import sql + + +class WriteFunctions: + @staticmethod + def create_season(season: 'models.Season'): + return 'Season created.' + + @staticmethod + def update_enemy(enemy: 'models.Enemy', *_): + (sql.Enemy + .insert(id=enemy.id, boss=enemy.boss, name=enemy.name, season=enemy.season) + .on_conflict(update={sql.Enemy.name: enemy.name, + sql.Enemy.boss: enemy.boss, + sql.Enemy.season: enemy.season}) + .execute()) + + @staticmethod + def update_player(player: 'models.Player', *_): + (sql.Player + .insert(id=player.id, name=player.name, hex_id=player.hex_id) + .on_conflict(update={sql.Player.name: player.name, + sql.Player.hex_id: player.hex_id}) + .execute()) + + @staticmethod + def update_drink(drink: 'models.Drink', *_): + (sql.Drink + .insert(id=drink.id, name=drink.name, vol=drink.vol) + .on_conflict(update={sql.Drink.name: drink.name, + sql.Drink.vol: drink.vol}) + .execute()) + + @staticmethod + def save_death(death: 'models.Death'): + with sql.db.atomic(): + created_id = (sql.Death + .insert(info=death.info, player=death.player, enemy=death.enemy, episode=death.episode, + time=death.time) + .execute()) + for penalty in death.penalties: + sql.Penalty.create(death=created_id, size=penalty.size, drink=penalty.drink, player=penalty.player) + + @staticmethod + def save_victory(victory: 'models.Victory'): + (sql.Victory + .insert(info=victory.info, player=victory.player, enemy=victory.enemy, time=victory.time, + episode=victory.episode, id=victory.id) + .execute()) + + @staticmethod + def update_season(season: 'models.Season', *_): + (sql.Season + .insert(id=season.id, number=season.number, game_name=season.game_name, start_date=season.start_date, + end_date=season.end_date) + .on_conflict( + update={sql.Season.number: season.number, + sql.Season.game_name: season.game_name, + sql.Season.start_date: season.start_date, + sql.Season.end_date: season.end_date}) + .execute()) + + @staticmethod + def update_episode(episode: 'models.Episode', *_): + players = list(sql.Player.select().where(sql.Player.id << [player.id for player in episode.players])) + new_ep_id = (sql.Episode + .insert(id=episode.id, number=episode.number, seq_number=episode.number, name=episode.name, + date=episode.date, season=episode.season) + .on_conflict(update={sql.Episode.name: episode.name, + sql.Episode.seq_number: episode.seq_number, + sql.Episode.number: episode.number, + sql.Episode.date: episode.date, + sql.Episode.season: episode.season}) + .execute()) + db_episode = sql.Episode.get(sql.Episode.id == new_ep_id) + db_episode.players = players + db_episode.save() diff --git a/dsst/dsst_server/server.py b/dsst/dsst_server/server.py new file mode 100644 index 0000000..a074838 --- /dev/null +++ b/dsst/dsst_server/server.py @@ -0,0 +1,99 @@ +import json +import pickle +import socket + +import sys + +import os + +from common import util, models +from dsst_server import func_read, func_write, tokens +from dsst_server.func_proxy import FunctionProxy +from dsst_server.data_access import sql, sql_func + +PORT = 12345 +HOST = socket.gethostname() +BUFFER_SIZE = 1024 + + +class DsstServer: + def __init__(self, config={}): + self.socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + print('Created socket') + + self.socket_server.bind((HOST, PORT)) + print('Bound socket to {} on host {}'.format(PORT, HOST)) + + # Initialize database + db_config = config.get('database') + sql.db.init(db_config.get('db_name'), user=db_config.get('user'), password=db_config.get('password')) + sql_func.create_tables() + print('Database initialized ({})'.format(sql.db.database)) + + # Load access tokens and map them to their allowed methods + read_actions = util.list_class_methods(func_read.ReadFunctions) + write_actions = util.list_class_methods(func_write.WriteFunctions) + parm_access = { + 'r': read_actions, + 'rw': read_actions + write_actions + } + self.tokens = {token: parm_access[perms] for token, perms in tokens.TOKENS} + print('Loaded auth tokens: {}'.format(self.tokens.keys())) + + def run(self): + self.socket_server.listen(5) + print('Socket is listening') + + while True: + client, address = self.socket_server.accept() + try: + print('Connection from {}'.format(address)) + data = util.recv_msg(client) + request = pickle.loads(data) + print('Request: {}'.format(request)) + # Validate auth token in request + token = request.get('auth_token') + if token not in self.tokens: + util.send_msg(client, pickle.dumps({'success': False, 'message': 'Auth token invalid'})) + print('Rejected request from {}. Auth token invalid ({})'.format(address, token)) + continue + # Check read functions + action_name = request.get('action') + if action_name in self.tokens[token]: + action = getattr(FunctionProxy, action_name) + try: + value = action(*request.get('args')) + except Exception as e: + response = {'success': False, 'message': 'Exception was thrown on server.\n{}'.format(e)} + util.send_msg(client, pickle.dumps(response)) + raise + response = {'success': True, 'data': value} + util.send_msg(client, pickle.dumps(response)) + continue + else: + msg = 'Action does not exist on server ({})'.format(request.get('action')) + util.send_msg(client, pickle.dumps({'success': False, 'message': msg})) + except Exception as e: + print(e) + finally: + client.close() + print('Connection to client closed') + + +def load_config(config_path: str) -> dict: + with open(config_path) as config_file: + return json.load(config_file) + + +def main(): + config = os.path.join(os.path.expanduser('~'), '.config', 'dsst', 'server.json') + server = DsstServer(load_config(config)) + try: + server.run() + except KeyboardInterrupt: + print('Server stopped') + server.socket_server.close() + try: + sys.exit(0) + except SystemExit: + os._exit(0) diff --git a/dsst/dsst_server/tokens.py b/dsst/dsst_server/tokens.py new file mode 100644 index 0000000..57c1d90 --- /dev/null +++ b/dsst/dsst_server/tokens.py @@ -0,0 +1,5 @@ +# Define access tokens here +# i.E: { 'read': ['a', 'b''], +# 'write': ['a'] +# } +TOKENS = [('a', 'rw'), ('b', 'r')] \ No newline at end of file