From 8516650af49191fc9cec096a7bc5496758478084 Mon Sep 17 00:00:00 2001 From: luxick Date: Sat, 3 Mar 2018 14:36:41 +0100 Subject: [PATCH 01/13] Move to basic client/server structure. --- dsst/__main__.py | 5 +- dsst/{dsst_gtk3 => common}/__init__.py | 0 dsst/common/models.py | 77 +++++++++++++++++++ dsst/common/util.py | 28 +++++++ .../handlers => dsst_client}/__init__.py | 0 dsst/dsst_client/client.py | 28 +++++++ .../dsst_gtk3}/__init__.py | 0 dsst/{ => dsst_client}/dsst_gtk3/dialogs.py | 2 +- dsst/{ => dsst_client}/dsst_gtk3/gtk_ui.py | 7 +- .../dsst_gtk3/handlers/__init__.py | 0 .../dsst_gtk3/handlers/base_data_handlers.py | 4 +- .../dsst_gtk3/handlers/death_handlers.py | 3 +- .../dsst_gtk3/handlers/dialog_handlers.py | 4 +- .../dsst_gtk3/handlers/handlers.py | 3 +- .../dsst_gtk3/handlers/season_handlers.py | 4 +- .../dsst_gtk3/handlers/victory_handlers.py | 3 +- dsst/{ => dsst_client}/dsst_gtk3/reload.py | 13 +++- .../dsst_gtk3/resources/glade/dialogs.glade | 0 .../dsst_gtk3/resources/glade/window.glade | 0 dsst/{ => dsst_client}/dsst_gtk3/util.py | 0 dsst/dsst_server/__init__.py | 0 dsst/dsst_server/data_access/__init__.py | 0 .../data_access}/sql.py | 0 .../data_access}/sql_func.py | 2 +- dsst/dsst_server/server.py | 42 ++++++++++ 25 files changed, 207 insertions(+), 18 deletions(-) rename dsst/{dsst_gtk3 => common}/__init__.py (100%) create mode 100644 dsst/common/models.py create mode 100644 dsst/common/util.py rename dsst/{dsst_gtk3/handlers => dsst_client}/__init__.py (100%) create mode 100644 dsst/dsst_client/client.py rename dsst/{dsst_sql => dsst_client/dsst_gtk3}/__init__.py (100%) rename dsst/{ => dsst_client}/dsst_gtk3/dialogs.py (99%) rename dsst/{ => dsst_client}/dsst_gtk3/gtk_ui.py (98%) create mode 100644 dsst/dsst_client/dsst_gtk3/handlers/__init__.py rename dsst/{ => dsst_client}/dsst_gtk3/handlers/base_data_handlers.py (96%) rename dsst/{ => dsst_client}/dsst_gtk3/handlers/death_handlers.py (93%) rename dsst/{ => dsst_client}/dsst_gtk3/handlers/dialog_handlers.py (94%) rename dsst/{ => dsst_client}/dsst_gtk3/handlers/handlers.py (97%) rename dsst/{ => dsst_client}/dsst_gtk3/handlers/season_handlers.py (93%) rename dsst/{ => dsst_client}/dsst_gtk3/handlers/victory_handlers.py (92%) rename dsst/{ => dsst_client}/dsst_gtk3/reload.py (96%) rename dsst/{ => dsst_client}/dsst_gtk3/resources/glade/dialogs.glade (100%) rename dsst/{ => dsst_client}/dsst_gtk3/resources/glade/window.glade (100%) rename dsst/{ => dsst_client}/dsst_gtk3/util.py (100%) create mode 100644 dsst/dsst_server/__init__.py create mode 100644 dsst/dsst_server/data_access/__init__.py rename dsst/{dsst_sql => dsst_server/data_access}/sql.py (100%) rename dsst/{dsst_sql => dsst_server/data_access}/sql_func.py (98%) create mode 100644 dsst/dsst_server/server.py diff --git a/dsst/__main__.py b/dsst/__main__.py index 4475530..7c28945 100644 --- a/dsst/__main__.py +++ b/dsst/__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)) -from dsst_gtk3 import gtk_ui +import gtk_ui if __name__ == '__main__': gtk_ui.main() \ No newline at end of file diff --git a/dsst/dsst_gtk3/__init__.py b/dsst/common/__init__.py similarity index 100% rename from dsst/dsst_gtk3/__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..96d2493 --- /dev/null +++ b/dsst/common/models.py @@ -0,0 +1,77 @@ +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.optional = arg.get('optional') + 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') + + +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') \ No newline at end of file diff --git a/dsst/common/util.py b/dsst/common/util.py new file mode 100644 index 0000000..903be04 --- /dev/null +++ b/dsst/common/util.py @@ -0,0 +1,28 @@ +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 \ No newline at end of file diff --git a/dsst/dsst_gtk3/handlers/__init__.py b/dsst/dsst_client/__init__.py similarity index 100% rename from dsst/dsst_gtk3/handlers/__init__.py rename to dsst/dsst_client/__init__.py diff --git a/dsst/dsst_client/client.py b/dsst/dsst_client/client.py new file mode 100644 index 0000000..45db76c --- /dev/null +++ b/dsst/dsst_client/client.py @@ -0,0 +1,28 @@ +import socket + +try: + import cPickcle as pickle +except ImportError: + print('cPickle package not installed, falling back to pickle') + import pickle + +from common import util, models + +PORT = 12345 +HOST = 'europa' +BUFFER_SIZE = 1024 + +s = socket.socket() +s.connect((HOST, PORT)) + +try: + data = 'get_dummy' + message = pickle.dumps(data) + util.send_msg(s, message) + response = util.recv_msg(s) + result = pickle.loads(response) + print(result, result.__dict__) + +finally: + s.close() + diff --git a/dsst/dsst_sql/__init__.py b/dsst/dsst_client/dsst_gtk3/__init__.py similarity index 100% rename from dsst/dsst_sql/__init__.py rename to dsst/dsst_client/dsst_gtk3/__init__.py diff --git a/dsst/dsst_gtk3/dialogs.py b/dsst/dsst_client/dsst_gtk3/dialogs.py similarity index 99% rename from dsst/dsst_gtk3/dialogs.py rename to dsst/dsst_client/dsst_gtk3/dialogs.py index 10a517a..614bb7c 100644 --- a/dsst/dsst_gtk3/dialogs.py +++ b/dsst/dsst_client/dsst_gtk3/dialogs.py @@ -5,7 +5,7 @@ import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk from datetime import datetime -from dsst_sql import sql +import sql from dsst_gtk3 import util diff --git a/dsst/dsst_gtk3/gtk_ui.py b/dsst/dsst_client/dsst_gtk3/gtk_ui.py similarity index 98% rename from dsst/dsst_gtk3/gtk_ui.py rename to dsst/dsst_client/dsst_gtk3/gtk_ui.py index e987820..330a325 100644 --- a/dsst/dsst_gtk3/gtk_ui.py +++ b/dsst/dsst_client/dsst_gtk3/gtk_ui.py @@ -1,10 +1,13 @@ -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 +import sql_func +import sql class GtkUi: diff --git a/dsst/dsst_client/dsst_gtk3/handlers/__init__.py b/dsst/dsst_client/dsst_gtk3/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dsst/dsst_gtk3/handlers/base_data_handlers.py b/dsst/dsst_client/dsst_gtk3/handlers/base_data_handlers.py similarity index 96% rename from dsst/dsst_gtk3/handlers/base_data_handlers.py rename to dsst/dsst_client/dsst_gtk3/handlers/base_data_handlers.py index b2c4d61..36c2d51 100644 --- a/dsst/dsst_gtk3/handlers/base_data_handlers.py +++ b/dsst/dsst_client/dsst_gtk3/handlers/base_data_handlers.py @@ -1,5 +1,5 @@ -from dsst_gtk3 import dialogs, gtk_ui -from dsst_sql import sql +import sql +from dsst_gtk3 import dialogs class BaseDataHandlers: diff --git a/dsst/dsst_gtk3/handlers/death_handlers.py b/dsst/dsst_client/dsst_gtk3/handlers/death_handlers.py similarity index 93% rename from dsst/dsst_gtk3/handlers/death_handlers.py rename to dsst/dsst_client/dsst_gtk3/handlers/death_handlers.py index aa45acd..fa4cbff 100644 --- a/dsst/dsst_gtk3/handlers/death_handlers.py +++ b/dsst/dsst_client/dsst_gtk3/handlers/death_handlers.py @@ -1,5 +1,6 @@ from gi.repository import Gtk -from dsst_gtk3 import dialogs, gtk_ui + +from dsst_gtk3 import dialogs class DeathHandlers: diff --git a/dsst/dsst_gtk3/handlers/dialog_handlers.py b/dsst/dsst_client/dsst_gtk3/handlers/dialog_handlers.py similarity index 94% rename from dsst/dsst_gtk3/handlers/dialog_handlers.py rename to dsst/dsst_client/dsst_gtk3/handlers/dialog_handlers.py index 5204828..1f34514 100644 --- a/dsst/dsst_gtk3/handlers/dialog_handlers.py +++ b/dsst/dsst_client/dsst_gtk3/handlers/dialog_handlers.py @@ -1,5 +1,5 @@ -from dsst_gtk3 import dialogs, util, gtk_ui -from dsst_sql import sql +import sql +from dsst_gtk3 import dialogs, util class DialogHandlers: diff --git a/dsst/dsst_gtk3/handlers/handlers.py b/dsst/dsst_client/dsst_gtk3/handlers/handlers.py similarity index 97% rename from dsst/dsst_gtk3/handlers/handlers.py rename to dsst/dsst_client/dsst_gtk3/handlers/handlers.py index 8856cce..182e1c2 100644 --- a/dsst/dsst_gtk3/handlers/handlers.py +++ b/dsst/dsst_client/dsst_gtk3/handlers/handlers.py @@ -6,7 +6,8 @@ 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 +import sql_func +import sql class Handlers(SeasonHandlers, BaseDataHandlers, DialogHandlers, DeathHandlers, VictoryHandlers): diff --git a/dsst/dsst_gtk3/handlers/season_handlers.py b/dsst/dsst_client/dsst_gtk3/handlers/season_handlers.py similarity index 93% rename from dsst/dsst_gtk3/handlers/season_handlers.py rename to dsst/dsst_client/dsst_gtk3/handlers/season_handlers.py index 1d667f7..2d5bc6a 100644 --- a/dsst/dsst_gtk3/handlers/season_handlers.py +++ b/dsst/dsst_client/dsst_gtk3/handlers/season_handlers.py @@ -1,5 +1,5 @@ -from dsst_sql import sql -from dsst_gtk3 import dialogs, gtk_ui +from data_access import sql +from dsst_gtk3 import dialogs class SeasonHandlers: diff --git a/dsst/dsst_gtk3/handlers/victory_handlers.py b/dsst/dsst_client/dsst_gtk3/handlers/victory_handlers.py similarity index 92% rename from dsst/dsst_gtk3/handlers/victory_handlers.py rename to dsst/dsst_client/dsst_gtk3/handlers/victory_handlers.py index d130a34..a9a8001 100644 --- a/dsst/dsst_gtk3/handlers/victory_handlers.py +++ b/dsst/dsst_client/dsst_gtk3/handlers/victory_handlers.py @@ -1,5 +1,6 @@ from gi.repository import Gtk -from dsst_gtk3 import dialogs, gtk_ui + +from dsst_gtk3 import dialogs class VictoryHandlers: diff --git a/dsst/dsst_gtk3/reload.py b/dsst/dsst_client/dsst_gtk3/reload.py similarity index 96% rename from dsst/dsst_gtk3/reload.py rename to dsst/dsst_client/dsst_gtk3/reload.py index 6d1bca8..adff4ee 100644 --- a/dsst/dsst_gtk3/reload.py +++ b/dsst/dsst_client/dsst_gtk3/reload.py @@ -1,7 +1,8 @@ from collections import Counter + from gi.repository import Gtk -from dsst_gtk3 import gtk_ui -from dsst_sql import sql, sql_func + +from data_access import sql, sql_func from dsst_gtk3 import util @@ -113,4 +114,10 @@ def reload_episode_stats(builder: Gtk.Builder, app: 'gtk_ui.GtkUi', episode_id: 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 + builder.get_object('ep_enemy_name_label').set_text(f'{enemy_name} ({deaths} Deaths)') + + +def fill_list_store(store: Gtk.ListStore, models: list): + store.clear() + for model in models: + pass \ No newline at end of file diff --git a/dsst/dsst_gtk3/resources/glade/dialogs.glade b/dsst/dsst_client/dsst_gtk3/resources/glade/dialogs.glade similarity index 100% rename from dsst/dsst_gtk3/resources/glade/dialogs.glade rename to dsst/dsst_client/dsst_gtk3/resources/glade/dialogs.glade diff --git a/dsst/dsst_gtk3/resources/glade/window.glade b/dsst/dsst_client/dsst_gtk3/resources/glade/window.glade similarity index 100% rename from dsst/dsst_gtk3/resources/glade/window.glade rename to dsst/dsst_client/dsst_gtk3/resources/glade/window.glade diff --git a/dsst/dsst_gtk3/util.py b/dsst/dsst_client/dsst_gtk3/util.py similarity index 100% rename from dsst/dsst_gtk3/util.py rename to dsst/dsst_client/dsst_gtk3/util.py 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/data_access/__init__.py b/dsst/dsst_server/data_access/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dsst/dsst_sql/sql.py b/dsst/dsst_server/data_access/sql.py similarity index 100% rename from dsst/dsst_sql/sql.py rename to dsst/dsst_server/data_access/sql.py diff --git a/dsst/dsst_sql/sql_func.py b/dsst/dsst_server/data_access/sql_func.py similarity index 98% rename from dsst/dsst_sql/sql_func.py rename to dsst/dsst_server/data_access/sql_func.py index 11139ee..dd12793 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 data_access.sql import * def get_episodes_for_season(season_id: int) -> list: diff --git a/dsst/dsst_server/server.py b/dsst/dsst_server/server.py new file mode 100644 index 0000000..1802df1 --- /dev/null +++ b/dsst/dsst_server/server.py @@ -0,0 +1,42 @@ +import pickle +import socket + +from data_access import sql +from common import util, models + +PORT = 12345 +HOST = socket.gethostname() +BUFFER_SIZE = 1024 + + +class DsstServer: + def __init__(self): + self.socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + print('Created socket') + + self.socket_server.bind((HOST, PORT)) + print(f'Bound socket to {PORT} on host {HOST}') + + def run(self): + self.socket_server.listen(5) + print('Socket is listening') + + while True: + client, address = self.socket_server.accept() + try: + print(f'Connection from {address}') + data = util.recv_msg(client) + request = pickle.loads(data) + print(f'Received data: {request}') + dummy = models.Player() + dummy.name = 'Player 1' + dummy.hex_id = '0xC2' + dummy.deaths = [1, 2, 3] + util.send_msg(client, pickle.dumps(dummy)) + finally: + client.close() + print('Connection to client closed') + +if __name__ == '__main__': + server = DsstServer() + server.run() From 25d237e81e8750e26082ad6fbb924ac9aef7ec8b Mon Sep 17 00:00:00 2001 From: luxick Date: Sat, 3 Mar 2018 18:52:13 +0100 Subject: [PATCH 02/13] Data loading over socket. --- dsst/common/util.py | 6 ++- dsst/dsst_client/client.py | 28 ---------- .../dsst_gtk3/handlers/__init__.py | 0 dsst/{dsst_client => dsst_gtk3}/__init__.py | 0 dsst/dsst_gtk3/client.py | 40 ++++++++++++++ dsst/{dsst_client => }/dsst_gtk3/dialogs.py | 0 dsst/{dsst_client => }/dsst_gtk3/gtk_ui.py | 0 .../handlers}/__init__.py | 0 .../dsst_gtk3/handlers/base_data_handlers.py | 0 .../dsst_gtk3/handlers/death_handlers.py | 0 .../dsst_gtk3/handlers/dialog_handlers.py | 0 .../dsst_gtk3/handlers/handlers.py | 0 .../dsst_gtk3/handlers/season_handlers.py | 0 .../dsst_gtk3/handlers/victory_handlers.py | 0 dsst/{dsst_client => }/dsst_gtk3/reload.py | 0 .../dsst_gtk3/resources/glade/dialogs.glade | 0 .../dsst_gtk3/resources/glade/window.glade | 0 dsst/{dsst_client => }/dsst_gtk3/util.py | 0 dsst/dsst_server/data_access/mapping.py | 33 ++++++++++++ dsst/dsst_server/func_proxy.py | 6 +++ dsst/dsst_server/read_functions.py | 19 +++++++ dsst/dsst_server/server.py | 52 ++++++++++++++++--- dsst/dsst_server/write_functions.py | 8 +++ 23 files changed, 155 insertions(+), 37 deletions(-) delete mode 100644 dsst/dsst_client/client.py delete mode 100644 dsst/dsst_client/dsst_gtk3/handlers/__init__.py rename dsst/{dsst_client => dsst_gtk3}/__init__.py (100%) create mode 100644 dsst/dsst_gtk3/client.py rename dsst/{dsst_client => }/dsst_gtk3/dialogs.py (100%) rename dsst/{dsst_client => }/dsst_gtk3/gtk_ui.py (100%) rename dsst/{dsst_client/dsst_gtk3 => dsst_gtk3/handlers}/__init__.py (100%) rename dsst/{dsst_client => }/dsst_gtk3/handlers/base_data_handlers.py (100%) rename dsst/{dsst_client => }/dsst_gtk3/handlers/death_handlers.py (100%) rename dsst/{dsst_client => }/dsst_gtk3/handlers/dialog_handlers.py (100%) rename dsst/{dsst_client => }/dsst_gtk3/handlers/handlers.py (100%) rename dsst/{dsst_client => }/dsst_gtk3/handlers/season_handlers.py (100%) rename dsst/{dsst_client => }/dsst_gtk3/handlers/victory_handlers.py (100%) rename dsst/{dsst_client => }/dsst_gtk3/reload.py (100%) rename dsst/{dsst_client => }/dsst_gtk3/resources/glade/dialogs.glade (100%) rename dsst/{dsst_client => }/dsst_gtk3/resources/glade/window.glade (100%) rename dsst/{dsst_client => }/dsst_gtk3/util.py (100%) create mode 100644 dsst/dsst_server/data_access/mapping.py create mode 100644 dsst/dsst_server/func_proxy.py create mode 100644 dsst/dsst_server/read_functions.py create mode 100644 dsst/dsst_server/write_functions.py diff --git a/dsst/common/util.py b/dsst/common/util.py index 903be04..8c52538 100644 --- a/dsst/common/util.py +++ b/dsst/common/util.py @@ -25,4 +25,8 @@ def recvall(sock, n): if not packet: return None data += packet - return data \ No newline at end of file + return data + + +def list_class_methods(class_obj): + return [name for name in dir(class_obj) if not name.startswith('__')] diff --git a/dsst/dsst_client/client.py b/dsst/dsst_client/client.py deleted file mode 100644 index 45db76c..0000000 --- a/dsst/dsst_client/client.py +++ /dev/null @@ -1,28 +0,0 @@ -import socket - -try: - import cPickcle as pickle -except ImportError: - print('cPickle package not installed, falling back to pickle') - import pickle - -from common import util, models - -PORT = 12345 -HOST = 'europa' -BUFFER_SIZE = 1024 - -s = socket.socket() -s.connect((HOST, PORT)) - -try: - data = 'get_dummy' - message = pickle.dumps(data) - util.send_msg(s, message) - response = util.recv_msg(s) - result = pickle.loads(response) - print(result, result.__dict__) - -finally: - s.close() - diff --git a/dsst/dsst_client/dsst_gtk3/handlers/__init__.py b/dsst/dsst_client/dsst_gtk3/handlers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/dsst/dsst_client/__init__.py b/dsst/dsst_gtk3/__init__.py similarity index 100% rename from dsst/dsst_client/__init__.py rename to dsst/dsst_gtk3/__init__.py diff --git a/dsst/dsst_gtk3/client.py b/dsst/dsst_gtk3/client.py new file mode 100644 index 0000000..d024914 --- /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, connection): + self.host = connection.get('host') + self.port = connection.get('port') + self.buffer = connection.get('buffer_size') + self.auth_key = connection.get('auth_key') + self.socket = socket.socket() + + def send_request(self, action: str, *args): + request = {'auth_key': self.auth_key, + 'action': action, + 'args': args} + request = pickle.dumps(request) + try: + self.socket.connect((self.host, self.port)) + util.send_msg(self.socket, request) + response = util.recv_msg(self.socket) + response = pickle.loads(response) + if not response.get('success'): + raise Exception(response.get('message')) + finally: + self.socket.close() + return response.get('data') + +if __name__ == '__main__': + access = Access({'host': 'europa', 'port': 12345, 'buffer_size': 1024, 'auth_key': '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_client/dsst_gtk3/dialogs.py b/dsst/dsst_gtk3/dialogs.py similarity index 100% rename from dsst/dsst_client/dsst_gtk3/dialogs.py rename to dsst/dsst_gtk3/dialogs.py diff --git a/dsst/dsst_client/dsst_gtk3/gtk_ui.py b/dsst/dsst_gtk3/gtk_ui.py similarity index 100% rename from dsst/dsst_client/dsst_gtk3/gtk_ui.py rename to dsst/dsst_gtk3/gtk_ui.py diff --git a/dsst/dsst_client/dsst_gtk3/__init__.py b/dsst/dsst_gtk3/handlers/__init__.py similarity index 100% rename from dsst/dsst_client/dsst_gtk3/__init__.py rename to dsst/dsst_gtk3/handlers/__init__.py diff --git a/dsst/dsst_client/dsst_gtk3/handlers/base_data_handlers.py b/dsst/dsst_gtk3/handlers/base_data_handlers.py similarity index 100% rename from dsst/dsst_client/dsst_gtk3/handlers/base_data_handlers.py rename to dsst/dsst_gtk3/handlers/base_data_handlers.py diff --git a/dsst/dsst_client/dsst_gtk3/handlers/death_handlers.py b/dsst/dsst_gtk3/handlers/death_handlers.py similarity index 100% rename from dsst/dsst_client/dsst_gtk3/handlers/death_handlers.py rename to dsst/dsst_gtk3/handlers/death_handlers.py diff --git a/dsst/dsst_client/dsst_gtk3/handlers/dialog_handlers.py b/dsst/dsst_gtk3/handlers/dialog_handlers.py similarity index 100% rename from dsst/dsst_client/dsst_gtk3/handlers/dialog_handlers.py rename to dsst/dsst_gtk3/handlers/dialog_handlers.py diff --git a/dsst/dsst_client/dsst_gtk3/handlers/handlers.py b/dsst/dsst_gtk3/handlers/handlers.py similarity index 100% rename from dsst/dsst_client/dsst_gtk3/handlers/handlers.py rename to dsst/dsst_gtk3/handlers/handlers.py diff --git a/dsst/dsst_client/dsst_gtk3/handlers/season_handlers.py b/dsst/dsst_gtk3/handlers/season_handlers.py similarity index 100% rename from dsst/dsst_client/dsst_gtk3/handlers/season_handlers.py rename to dsst/dsst_gtk3/handlers/season_handlers.py diff --git a/dsst/dsst_client/dsst_gtk3/handlers/victory_handlers.py b/dsst/dsst_gtk3/handlers/victory_handlers.py similarity index 100% rename from dsst/dsst_client/dsst_gtk3/handlers/victory_handlers.py rename to dsst/dsst_gtk3/handlers/victory_handlers.py diff --git a/dsst/dsst_client/dsst_gtk3/reload.py b/dsst/dsst_gtk3/reload.py similarity index 100% rename from dsst/dsst_client/dsst_gtk3/reload.py rename to dsst/dsst_gtk3/reload.py diff --git a/dsst/dsst_client/dsst_gtk3/resources/glade/dialogs.glade b/dsst/dsst_gtk3/resources/glade/dialogs.glade similarity index 100% rename from dsst/dsst_client/dsst_gtk3/resources/glade/dialogs.glade rename to dsst/dsst_gtk3/resources/glade/dialogs.glade diff --git a/dsst/dsst_client/dsst_gtk3/resources/glade/window.glade b/dsst/dsst_gtk3/resources/glade/window.glade similarity index 100% rename from dsst/dsst_client/dsst_gtk3/resources/glade/window.glade rename to dsst/dsst_gtk3/resources/glade/window.glade diff --git a/dsst/dsst_client/dsst_gtk3/util.py b/dsst/dsst_gtk3/util.py similarity index 100% rename from dsst/dsst_client/dsst_gtk3/util.py rename to dsst/dsst_gtk3/util.py diff --git a/dsst/dsst_server/data_access/mapping.py b/dsst/dsst_server/data_access/mapping.py new file mode 100644 index 0000000..0c724ad --- /dev/null +++ b/dsst/dsst_server/data_access/mapping.py @@ -0,0 +1,33 @@ +from dsst_server.data_access import sql +from common import models + + +def map_base_fields(cls, db_model): + model = cls() + attrs = [attr for attr in db_model._meta.fields] + for attr in attrs: + setattr(model, attr, getattr(db_model, attr)) + return model + + +def db_to_enemy(enemy: 'sql.Enemy'): + return map_base_fields(models.Enemy, enemy) + + +def db_to_player(player: 'sql.Player'): + model = map_base_fields(models.Player, player) + 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 = [] + model.victories = [] + + +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_server/func_proxy.py b/dsst/dsst_server/func_proxy.py new file mode 100644 index 0000000..754ca0d --- /dev/null +++ b/dsst/dsst_server/func_proxy.py @@ -0,0 +1,6 @@ +from dsst_server.write_functions import WriteFunctions +from dsst_server.read_functions import ReadFunctions + + +class FunctionProxy(WriteFunctions, ReadFunctions): + pass diff --git a/dsst/dsst_server/read_functions.py b/dsst/dsst_server/read_functions.py new file mode 100644 index 0000000..e7c5187 --- /dev/null +++ b/dsst/dsst_server/read_functions.py @@ -0,0 +1,19 @@ +from dsst_server.data_access import sql, sql_func, mapping +from common import models +from playhouse import shortcuts + + +class ReadFunctions: + + @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_players(*_): + return [mapping.db_to_player(player) for player in sql.Player.select()] + diff --git a/dsst/dsst_server/server.py b/dsst/dsst_server/server.py index 1802df1..1eb5a3d 100644 --- a/dsst/dsst_server/server.py +++ b/dsst/dsst_server/server.py @@ -1,8 +1,14 @@ import pickle import socket -from data_access import sql +import sys + +import os + from common import util, models +from dsst_server import read_functions, write_functions +from dsst_server.func_proxy import FunctionProxy +from dsst_server.data_access import sql PORT = 12345 HOST = socket.gethostname() @@ -17,6 +23,13 @@ class DsstServer: self.socket_server.bind((HOST, PORT)) print(f'Bound socket to {PORT} on host {HOST}') + self.read_actions = util.list_class_methods(read_functions.ReadFunctions) + self.write_actions = util.list_class_methods(write_functions.WriteFunctions) + sql.db.init('dsst', user='dsst', password='dsst') + + self.key_access = {'a': self.read_actions, + 'b': self.read_actions + self.write_actions} + def run(self): self.socket_server.listen(5) print('Socket is listening') @@ -27,16 +40,39 @@ class DsstServer: print(f'Connection from {address}') data = util.recv_msg(client) request = pickle.loads(data) - print(f'Received data: {request}') - dummy = models.Player() - dummy.name = 'Player 1' - dummy.hex_id = '0xC2' - dummy.deaths = [1, 2, 3] - util.send_msg(client, pickle.dumps(dummy)) + print(f'Request: {request}') + # Validate auth key in request + key = request.get('auth_key') + if key not in self.key_access: + util.send_msg(client, pickle.dumps({'success': False, 'message': 'Auth Key invalid'})) + print(f'Rejected request from {address}. Auth key invalid ({key})') + continue + # Check read functions + action_name = request.get('action') + if action_name in self.key_access[key]: + action = getattr(FunctionProxy, action_name) + value = action(request.get('args')) + response = {'success': True, 'data': value} + util.send_msg(client, pickle.dumps(response)) + continue + else: + msg = f'Action does not exist on server ({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') + if __name__ == '__main__': server = DsstServer() - server.run() + 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/write_functions.py b/dsst/dsst_server/write_functions.py new file mode 100644 index 0000000..ea27bdf --- /dev/null +++ b/dsst/dsst_server/write_functions.py @@ -0,0 +1,8 @@ +from common import models + + +class WriteFunctions: + + @staticmethod + def create_season(season: 'models.Season'): + return 'Season created.' From 8b0422b1b0d3ed3221156bc953a838d6dcefed9e Mon Sep 17 00:00:00 2001 From: luxick Date: Mon, 5 Mar 2018 22:57:14 +0100 Subject: [PATCH 03/13] Additional loading functions. --- dsst/dsst_gtk3/client.py | 32 +++++++-------- dsst/dsst_gtk3/gtk_ui.py | 54 ++++++++++++++++--------- dsst/dsst_gtk3/handlers/handlers.py | 1 - dsst/dsst_gtk3/reload.py | 15 ++++--- dsst/dsst_gtk3/util.py | 22 +++++++--- dsst/dsst_server/data_access/mapping.py | 32 +++++++++++++-- dsst/dsst_server/read_functions.py | 9 +++++ dsst/dsst_server/server.py | 36 +++++++++++------ dsst/dsst_server/tokens.py | 5 +++ 9 files changed, 140 insertions(+), 66 deletions(-) create mode 100644 dsst/dsst_server/tokens.py diff --git a/dsst/dsst_gtk3/client.py b/dsst/dsst_gtk3/client.py index d024914..b7da4b3 100644 --- a/dsst/dsst_gtk3/client.py +++ b/dsst/dsst_gtk3/client.py @@ -8,31 +8,31 @@ except ImportError: class Access: - def __init__(self, connection): - self.host = connection.get('host') - self.port = connection.get('port') - self.buffer = connection.get('buffer_size') - self.auth_key = connection.get('auth_key') - self.socket = socket.socket() + 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_key': self.auth_key, + request = {'auth_token': self.auth_token, 'action': action, 'args': args} request = pickle.dumps(request) + soc = socket.socket() try: - self.socket.connect((self.host, self.port)) - util.send_msg(self.socket, request) - response = util.recv_msg(self.socket) - response = pickle.loads(response) - if not response.get('success'): - raise Exception(response.get('message')) + 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: - self.socket.close() - return response.get('data') + soc.close() + return message.get('data') if __name__ == '__main__': - access = Access({'host': 'europa', 'port': 12345, 'buffer_size': 1024, 'auth_key': 'a'}) + 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) diff --git a/dsst/dsst_gtk3/gtk_ui.py b/dsst/dsst_gtk3/gtk_ui.py index 330a325..1a2c364 100644 --- a/dsst/dsst_gtk3/gtk_ui.py +++ b/dsst/dsst_gtk3/gtk_ui.py @@ -5,7 +5,7 @@ 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_gtk3 import util, reload, client import sql_func import sql @@ -26,27 +26,41 @@ class GtkUi: 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) + self.data = {} + # Load base data and seasons + self.initial_load() + + def initial_load(self): + with util.handle_exception(Exception): + self.data['players'] = self.data_client.send_request('load_players') + self.data['drinks'] = self.data_client.send_request('load_drinks') + self.data['seasons'] = self.data_client.send_request('load_seasons') + reload.reload_base_data(self.ui, 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) + with util.handle_exception(Exception): + self.data['episodes'] = self.data_client.send_request('load_episodes', self.get_selected_season_id()) + reload.reload_episodes(self.ui, self) + pass + # 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) + + def load(self, data_dict: dict, value_field: str, request_name: str): + try: + data_dict[value_field] = self.data_client.send_request('request_name') + except Exception as e: + print() def set_db_status_label(self, db_conf: dict): self.ui.get_object('connection_label').set_text(f'{db_conf["user"]}@{db_conf["host"]}') diff --git a/dsst/dsst_gtk3/handlers/handlers.py b/dsst/dsst_gtk3/handlers/handlers.py index 182e1c2..748d5ae 100644 --- a/dsst/dsst_gtk3/handlers/handlers.py +++ b/dsst/dsst_gtk3/handlers/handlers.py @@ -29,7 +29,6 @@ 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 ################################################################################################## diff --git a/dsst/dsst_gtk3/reload.py b/dsst/dsst_gtk3/reload.py index adff4ee..d352e3b 100644 --- a/dsst/dsst_gtk3/reload.py +++ b/dsst/dsst_gtk3/reload.py @@ -3,21 +3,21 @@ from collections import Counter from gi.repository import Gtk from data_access 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(builder: Gtk.Builder, 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(): + for player in app.data['players']: builder.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(): + for drink in app.data['drinks']: builder.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 @@ -25,23 +25,22 @@ def reload_base_data(builder: Gtk.Builder, app: 'gtk_ui.GtkUi'): with util.block_handler(combo, app.handlers.do_season_selected): store = builder.get_object('seasons_store') store.clear() - for season in sql.Season.select().order_by(sql.Season.number): + for season in app.data['seasons']: 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(builder: Gtk.Builder, 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() 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.data['episodes']: model.append([episode.id, episode.name, str(episode.date), episode.number]) if selected_paths: selection.select_path(selected_paths[0]) diff --git a/dsst/dsst_gtk3/util.py b/dsst/dsst_gtk3/util.py index 391525f..39d921e 100644 --- a/dsst/dsst_gtk3/util.py +++ b/dsst/dsst_gtk3/util.py @@ -5,18 +5,17 @@ import json import os from contextlib import contextmanager from gi.repository import Gtk -from typing import Callable +from typing import Callable, Type 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': 'a'} ] } @@ -32,6 +31,17 @@ def block_handler(widget: 'Gtk.Widget', handler_func: Callable): widget.handler_unblock_by_func(handler_func) +@contextmanager +def handle_exception(exception: 'Type[Exception]'): + """Run operation in try/except block and display exception in a dialog + :param exception: + """ + try: + yield + except exception as e: + print(e) + + 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()' diff --git a/dsst/dsst_server/data_access/mapping.py b/dsst/dsst_server/data_access/mapping.py index 0c724ad..e8052a7 100644 --- a/dsst/dsst_server/data_access/mapping.py +++ b/dsst/dsst_server/data_access/mapping.py @@ -10,20 +10,46 @@ def map_base_fields(cls, db_model): 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'): - model = map_base_fields(models.Player, 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 = [] - model.victories = [] + 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'): diff --git a/dsst/dsst_server/read_functions.py b/dsst/dsst_server/read_functions.py index e7c5187..7e79272 100644 --- a/dsst/dsst_server/read_functions.py +++ b/dsst/dsst_server/read_functions.py @@ -13,7 +13,16 @@ class ReadFunctions: 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_drinks(*_): + return [mapping.db_to_drink(drink) for drink in sql.Drink.select()] \ No newline at end of file diff --git a/dsst/dsst_server/server.py b/dsst/dsst_server/server.py index 1eb5a3d..d48aedb 100644 --- a/dsst/dsst_server/server.py +++ b/dsst/dsst_server/server.py @@ -6,7 +6,7 @@ import sys import os from common import util, models -from dsst_server import read_functions, write_functions +from dsst_server import read_functions, write_functions, tokens from dsst_server.func_proxy import FunctionProxy from dsst_server.data_access import sql @@ -23,12 +23,19 @@ class DsstServer: self.socket_server.bind((HOST, PORT)) print(f'Bound socket to {PORT} on host {HOST}') - self.read_actions = util.list_class_methods(read_functions.ReadFunctions) - self.write_actions = util.list_class_methods(write_functions.WriteFunctions) + # Initialize database sql.db.init('dsst', user='dsst', password='dsst') + print(f'Database initialized ({sql.db.database})') - self.key_access = {'a': self.read_actions, - 'b': self.read_actions + self.write_actions} + # Load access tokens and map them to their allowed methods + read_actions = util.list_class_methods(read_functions.ReadFunctions) + write_actions = util.list_class_methods(write_functions.WriteFunctions) + parm_access = { + 'r': read_actions, + 'rw': read_actions + write_actions + } + self.tokens = {token: parm_access[perms] for token, perms in tokens.TOKENS} + print(f'Loaded auth tokens: {self.tokens.keys()}') def run(self): self.socket_server.listen(5) @@ -41,17 +48,22 @@ class DsstServer: data = util.recv_msg(client) request = pickle.loads(data) print(f'Request: {request}') - # Validate auth key in request - key = request.get('auth_key') - if key not in self.key_access: - util.send_msg(client, pickle.dumps({'success': False, 'message': 'Auth Key invalid'})) - print(f'Rejected request from {address}. Auth key invalid ({key})') + # 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(f'Rejected request from {address}. Auth token invalid ({token})') continue # Check read functions action_name = request.get('action') - if action_name in self.key_access[key]: + if action_name in self.tokens[token]: action = getattr(FunctionProxy, action_name) - value = action(request.get('args')) + try: + value = action(request.get('args')) + except Exception as e: + response = {'success': False, 'message': f'Exception was thrown on server.\n{e}'} + util.send_msg(client, pickle.dumps(response)) + raise response = {'success': True, 'data': value} util.send_msg(client, pickle.dumps(response)) continue 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 From e5d0d92de24a90e16ca20605f227382d69fe1449 Mon Sep 17 00:00:00 2001 From: luxick Date: Wed, 7 Mar 2018 15:26:22 +0100 Subject: [PATCH 04/13] Update build script for client/server use. --- build.py | 61 ++++- dsst/common/models.py | 10 +- dsst/{ => dsst_gtk3}/__main__.py | 6 +- dsst/dsst_gtk3/dialogs.py | 248 +++++++++--------- dsst/dsst_gtk3/gtk_ui.py | 52 ++-- dsst/dsst_gtk3/handlers/base_data_handlers.py | 27 +- dsst/dsst_gtk3/handlers/death_handlers.py | 1 - dsst/dsst_gtk3/handlers/dialog_handlers.py | 11 +- dsst/dsst_gtk3/handlers/handlers.py | 5 +- dsst/dsst_gtk3/handlers/season_handlers.py | 6 +- dsst/dsst_gtk3/handlers/victory_handlers.py | 1 - dsst/dsst_gtk3/reload.py | 57 ++-- dsst/dsst_gtk3/resources/glade/window.glade | 29 +- dsst/dsst_gtk3/util.py | 12 +- dsst/dsst_server/__main__.py | 11 + dsst/dsst_server/data_access/mapping.py | 13 +- dsst/dsst_server/data_access/sql.py | 10 +- dsst/dsst_server/data_access/sql_func.py | 30 ++- dsst/dsst_server/read_functions.py | 21 +- dsst/dsst_server/server.py | 35 ++- 20 files changed, 382 insertions(+), 264 deletions(-) rename dsst/{ => dsst_gtk3}/__main__.py (57%) create mode 100644 dsst/dsst_server/__main__.py diff --git a/build.py b/build.py index a0ddd2f..3b2ad30 100644 --- a/build.py +++ b/build.py @@ -3,16 +3,59 @@ 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/common/models.py b/dsst/common/models.py index 96d2493..9e29bd7 100644 --- a/dsst/common/models.py +++ b/dsst/common/models.py @@ -45,7 +45,7 @@ class Enemy: def __init__(self, arg={}): self.id = arg.get('id') self.name = arg.get('name') - self.optional = arg.get('optional') + self.boss = arg.get('boss') self.season = arg.get('season') @@ -74,4 +74,10 @@ class Victory: self.info = arg.get('info') self.player = arg.get('player') self.enemy = arg.get('enemy') - self.episode = arg.get('episode') \ No newline at end of file + self.episode = arg.get('episode') + + +class SeasonStats: + def __init__(self, arg={}): + self.player_kd = arg.get('player_kd') + self.enemies = arg.get('enemies') diff --git a/dsst/__main__.py b/dsst/dsst_gtk3/__main__.py similarity index 57% rename from dsst/__main__.py rename to dsst/dsst_gtk3/__main__.py index 7c28945..dcb106a 100644 --- a/dsst/__main__.py +++ b/dsst/dsst_gtk3/__main__.py @@ -3,9 +3,9 @@ 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))) -import gtk_ui +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/dialogs.py b/dsst/dsst_gtk3/dialogs.py index 614bb7c..ded4d30 100644 --- a/dsst/dsst_gtk3/dialogs.py +++ b/dsst/dsst_gtk3/dialogs.py @@ -1,12 +1,7 @@ """ This module contains UI functions for displaying different dialogs """ -import gi -gi.require_version('Gtk', '3.0') from gi.repository import Gtk -from datetime import datetime -import sql -from dsst_gtk3 import util def enter_string_dialog(builder: Gtk.Builder, title: str, value=None) -> str: @@ -33,7 +28,7 @@ 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): +def show_episode_dialog(builder: Gtk.Builder, title: str, season_id: int, episode): """ Shows a dialog to edit an episode :param builder: GtkBuilder with loaded 'dialogs.glade' :param title: Title of the dialog window @@ -41,44 +36,45 @@ def show_episode_dialog(builder: Gtk.Builder, title: str, season_id: int, episod :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]) - - 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 + pass + # # 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]) + # + # 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): @@ -106,86 +102,88 @@ def show_manage_drinks_dialog(builder: Gtk.Builder): dialog.hide() -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 - """ - 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) - - # 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.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_death_dialog(builder: Gtk.Builder, episode_id: int, death): + pass + # """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 + # """ + # 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) + # + # # 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.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 +def show_edit_victory_dialog(builder: Gtk.Builder, episode_id: int, victory): + pass + # """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 diff --git a/dsst/dsst_gtk3/gtk_ui.py b/dsst/dsst_gtk3/gtk_ui.py index 1a2c364..6f9494a 100644 --- a/dsst/dsst_gtk3/gtk_ui.py +++ b/dsst/dsst_gtk3/gtk_ui.py @@ -1,13 +1,9 @@ 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, client -import sql_func -import sql class GtkUi: @@ -30,41 +26,37 @@ class GtkUi: config = config['servers'][0] self.data_client = client.Access(config) self.data = {} + self.meta = {'connection': '{}:{}'.format(config.get('host'), config.get('port'))} + self.season_changed = True + self.ep_changed = False # Load base data and seasons self.initial_load() + self.update_status_bar_meta() def initial_load(self): - with util.handle_exception(Exception): + with util.network_operation(self): self.data['players'] = self.data_client.send_request('load_players') self.data['drinks'] = self.data_client.send_request('load_drinks') self.data['seasons'] = self.data_client.send_request('load_seasons') - reload.reload_base_data(self.ui, self) + self.meta['database'] = self.data_client.send_request('load_db_meta') + reload.reload_base_data(self.ui, self) def reload(self): - with util.handle_exception(Exception): - self.data['episodes'] = self.data_client.send_request('load_episodes', self.get_selected_season_id()) - reload.reload_episodes(self.ui, self) - pass - # 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) + if self.season_changed: + with util.network_operation(self): + season_id = self.get_selected_season_id() + self.data['episodes'] = self.data_client.send_request('load_episodes', season_id) + self.data['season_stats'] = self.data_client.send_request('load_season_stats', season_id) + reload.reload_episodes(self.ui, self) + reload.reload_season_stats(self) + self.season_changed = False - def load(self, data_dict: dict, value_field: str, request_name: str): - try: - data_dict[value_field] = self.data_client.send_request('request_name') - except Exception as e: - print() + if self.ep_changed: + reload.reload_episode_stats(self) - 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_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 @@ -87,3 +79,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 36c2d51..5efa887 100644 --- a/dsst/dsst_gtk3/handlers/base_data_handlers.py +++ b/dsst/dsst_gtk3/handlers/base_data_handlers.py @@ -1,4 +1,3 @@ -import sql from dsst_gtk3 import dialogs @@ -12,7 +11,7 @@ class BaseDataHandlers: def do_add_player(self, entry): if entry.get_text(): - sql.Player.create(name=entry.get_text()) + # sql.Player.create(name=entry.get_text()) entry.set_text('') self.app.reload() @@ -21,16 +20,16 @@ class BaseDataHandlers: 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() + # sql.Player.update(name=value)\ + # .where(sql.Player.id == row[0])\ + # .execute() self.app.reload() 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() + # sql.Player.update(hex_id=value)\ + # .where(sql.Player.id == row[0])\ + # .execute() self.app.reload() def do_add_drink(self, entry): @@ -41,14 +40,14 @@ class BaseDataHandlers: 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() + # sql.Drink.update(name=value)\ + # .where(sql.Drink.id == row[0])\ + # .execute() self.app.reload() 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() + # sql.Drink.update(vol=value) \ + # .where(sql.Drink.id == row[0]) \ + # .execute() self.app.reload() \ 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 fa4cbff..410b3a0 100644 --- a/dsst/dsst_gtk3/handlers/death_handlers.py +++ b/dsst/dsst_gtk3/handlers/death_handlers.py @@ -1,5 +1,4 @@ from gi.repository import Gtk - from dsst_gtk3 import dialogs diff --git a/dsst/dsst_gtk3/handlers/dialog_handlers.py b/dsst/dsst_gtk3/handlers/dialog_handlers.py index 1f34514..1224b50 100644 --- a/dsst/dsst_gtk3/handlers/dialog_handlers.py +++ b/dsst/dsst_gtk3/handlers/dialog_handlers.py @@ -1,4 +1,3 @@ -import sql from dsst_gtk3 import dialogs, util @@ -14,16 +13,16 @@ 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 = sql.Player.get(sql.Player.id == 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]) + # if not any(row[0] == player_id for row in store): + # store.append([player_id, player.name, player.hex_id]) 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 = sql.Enemy.create(name=entry.get_text(), season=self.app.get_selected_season_id()) + # store.append([enemy.name, False, 0, enemy.id]) entry.set_text('') def do_manage_drinks(self, *_): diff --git a/dsst/dsst_gtk3/handlers/handlers.py b/dsst/dsst_gtk3/handlers/handlers.py index 748d5ae..73c93b4 100644 --- a/dsst/dsst_gtk3/handlers/handlers.py +++ b/dsst/dsst_gtk3/handlers/handlers.py @@ -6,8 +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 -import sql_func -import sql class Handlers(SeasonHandlers, BaseDataHandlers, DialogHandlers, DeathHandlers, VictoryHandlers): @@ -35,5 +33,4 @@ class Handlers(SeasonHandlers, BaseDataHandlers, DialogHandlers, DeathHandlers, @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 2d5bc6a..9734381 100644 --- a/dsst/dsst_gtk3/handlers/season_handlers.py +++ b/dsst/dsst_gtk3/handlers/season_handlers.py @@ -1,5 +1,4 @@ -from data_access import sql -from dsst_gtk3 import dialogs +from dsst_gtk3 import dialogs, gtk_ui class SeasonHandlers: @@ -10,10 +9,10 @@ class SeasonHandlers: 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() def do_season_selected(self, *_): + self.app.season_changed = True self.app.reload() def do_add_episode(self, *_): @@ -24,6 +23,7 @@ class SeasonHandlers: self.app.reload() def on_selected_episode_changed(self, *_): + self.app.ep_changed = True self.app.reload() def on_episode_double_click(self, *_): diff --git a/dsst/dsst_gtk3/handlers/victory_handlers.py b/dsst/dsst_gtk3/handlers/victory_handlers.py index a9a8001..8b1f9a5 100644 --- a/dsst/dsst_gtk3/handlers/victory_handlers.py +++ b/dsst/dsst_gtk3/handlers/victory_handlers.py @@ -1,5 +1,4 @@ from gi.repository import Gtk - from dsst_gtk3 import dialogs diff --git a/dsst/dsst_gtk3/reload.py b/dsst/dsst_gtk3/reload.py index d352e3b..e7d7d49 100644 --- a/dsst/dsst_gtk3/reload.py +++ b/dsst/dsst_gtk3/reload.py @@ -1,8 +1,5 @@ from collections import Counter - from gi.repository import Gtk - -from data_access import sql, sql_func from dsst_gtk3 import util, gtk_ui @@ -46,45 +43,35 @@ def reload_episodes(builder: Gtk.Builder, app: 'gtk_ui.GtkUi'): 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') + season_stats = app.data.get('season_stats') + # 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_name, deaths, defeated, boss in season_stats.enemies: + store.append([enemy_name, defeated, deaths, boss]) -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') + episode = [ep for ep in app.data['episodes'] if ep.id == app.get_selected_episode_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] @@ -92,28 +79,28 @@ def reload_episode_stats(builder: Gtk.Builder, app: 'gtk_ui.GtkUi', episode_id: penalty_string = ', '.join(penalties) store.append([death.id, death.player.name, death.enemy.name, penalty_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]) # 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)') + app.ui.get_object('ep_enemy_name_label').set_text(f'{enemy_name} ({deaths} Deaths)') def fill_list_store(store: Gtk.ListStore, models: list): diff --git a/dsst/dsst_gtk3/resources/glade/window.glade b/dsst/dsst_gtk3/resources/glade/window.glade index 9312066..02d2131 100644 --- a/dsst/dsst_gtk3/resources/glade/window.glade +++ b/dsst/dsst_gtk3/resources/glade/window.glade @@ -1,5 +1,5 @@ - + @@ -366,8 +366,8 @@ - - + + @@ -2264,6 +2264,7 @@ Name + True True True 0 @@ -2345,7 +2346,9 @@ + True Name + True True True 0 @@ -2360,6 +2363,7 @@ + True Deaths True True @@ -2367,12 +2371,28 @@ - 1 2 + + + 40 + Boss + True + True + 3 + + + checkmark + + + 3 + + + + @@ -2417,6 +2437,7 @@ Name + True diff --git a/dsst/dsst_gtk3/util.py b/dsst/dsst_gtk3/util.py index 39d921e..25d5142 100644 --- a/dsst/dsst_gtk3/util.py +++ b/dsst/dsst_gtk3/util.py @@ -5,7 +5,8 @@ import json import os from contextlib import contextmanager from gi.repository import Gtk -from typing import Callable, Type +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') @@ -32,14 +33,19 @@ def block_handler(widget: 'Gtk.Widget', handler_func: Callable): @contextmanager -def handle_exception(exception: 'Type[Exception]'): +def network_operation(app: 'gtk_ui.GtkUi'): """Run operation in try/except block and display exception in a dialog :param exception: """ + app.ui.get_object('status_bar').push(0, 'Connecting to server') try: yield - except exception as e: + 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): 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/mapping.py b/dsst/dsst_server/data_access/mapping.py index e8052a7..ceeb1dc 100644 --- a/dsst/dsst_server/data_access/mapping.py +++ b/dsst/dsst_server/data_access/mapping.py @@ -3,10 +3,21 @@ 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: - setattr(model, attr, getattr(db_model, attr)) + 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 diff --git a/dsst/dsst_server/data_access/sql.py b/dsst/dsst_server/data_access/sql.py index 16052c2..cd9391c 100644 --- a/dsst/dsst_server/data_access/sql.py +++ b/dsst/dsst_server/data_access/sql.py @@ -5,8 +5,12 @@ Example: from sql import Episode query = Episode.select().where(Episode.name == 'MyName') """ - -from peewee import * +import sys +try: + from peewee import * +except ImportError: + print('peewee package not installed') + sys.exit(0) db = MySQLDatabase(None) @@ -56,7 +60,7 @@ class Drink(Model): class Enemy(Model): id = AutoField() name = CharField() - optional = BooleanField() + boss = BooleanField() season = ForeignKeyField(Season, backref='enemies') class Meta: diff --git a/dsst/dsst_server/data_access/sql_func.py b/dsst/dsst_server/data_access/sql_func.py index dd12793..0166bdc 100644 --- a/dsst/dsst_server/data_access/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 data_access.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/read_functions.py b/dsst/dsst_server/read_functions.py index 7e79272..6a95f82 100644 --- a/dsst/dsst_server/read_functions.py +++ b/dsst/dsst_server/read_functions.py @@ -4,6 +4,9 @@ from playhouse import shortcuts class ReadFunctions: + @staticmethod + def load_db_meta(*_): + return sql.db.database @staticmethod def load_seasons(*_): @@ -25,4 +28,20 @@ class ReadFunctions: @staticmethod def load_drinks(*_): - return [mapping.db_to_drink(drink) for drink in sql.Drink.select()] \ No newline at end of file + 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.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/server.py b/dsst/dsst_server/server.py index d48aedb..5a0794d 100644 --- a/dsst/dsst_server/server.py +++ b/dsst/dsst_server/server.py @@ -1,3 +1,4 @@ +import json import pickle import socket @@ -8,7 +9,7 @@ import os from common import util, models from dsst_server import read_functions, write_functions, tokens from dsst_server.func_proxy import FunctionProxy -from dsst_server.data_access import sql +from dsst_server.data_access import sql, sql_func PORT = 12345 HOST = socket.gethostname() @@ -16,16 +17,18 @@ BUFFER_SIZE = 1024 class DsstServer: - def __init__(self): + 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(f'Bound socket to {PORT} on host {HOST}') + print('Bound socket to {} on host {}'.format(PORT, HOST)) # Initialize database - sql.db.init('dsst', user='dsst', password='dsst') - print(f'Database initialized ({sql.db.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(read_functions.ReadFunctions) @@ -35,7 +38,7 @@ class DsstServer: 'rw': read_actions + write_actions } self.tokens = {token: parm_access[perms] for token, perms in tokens.TOKENS} - print(f'Loaded auth tokens: {self.tokens.keys()}') + print('Loaded auth tokens: {}'.format(self.tokens.keys())) def run(self): self.socket_server.listen(5) @@ -44,15 +47,15 @@ class DsstServer: while True: client, address = self.socket_server.accept() try: - print(f'Connection from {address}') + print('Connection from {}'.format(address)) data = util.recv_msg(client) request = pickle.loads(data) - print(f'Request: {request}') + 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(f'Rejected request from {address}. Auth token invalid ({token})') + print('Rejected request from {}. Auth token invalid ({})'.format(address, token)) continue # Check read functions action_name = request.get('action') @@ -61,14 +64,14 @@ class DsstServer: try: value = action(request.get('args')) except Exception as e: - response = {'success': False, 'message': f'Exception was thrown on server.\n{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 = f'Action does not exist on server ({request.get("action")})' + 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) @@ -77,8 +80,14 @@ class DsstServer: print('Connection to client closed') -if __name__ == '__main__': - server = DsstServer() +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: From ce7bb3a244bdd571a9bf89528def04f42ef0dc07 Mon Sep 17 00:00:00 2001 From: luxick Date: Wed, 7 Mar 2018 18:38:29 +0100 Subject: [PATCH 05/13] Add logo to status bar. --- dsst/dsst_gtk3/gtk_ui.py | 6 +++- dsst/dsst_gtk3/resources/glade/window.glade | 18 ++++++++-- dsst/dsst_gtk3/resources/images/dd.png | Bin 0 -> 47632 bytes dsst/dsst_gtk3/util.py | 31 ++++++++++++++++-- dsst/dsst_server/func_proxy.py | 4 +-- .../{read_functions.py => func_read.py} | 0 .../{write_functions.py => func_write.py} | 0 dsst/dsst_server/server.py | 6 ++-- 8 files changed, 53 insertions(+), 12 deletions(-) create mode 100644 dsst/dsst_gtk3/resources/images/dd.png rename dsst/dsst_server/{read_functions.py => func_read.py} (100%) rename dsst/dsst_server/{write_functions.py => func_write.py} (100%) diff --git a/dsst/dsst_gtk3/gtk_ui.py b/dsst/dsst_gtk3/gtk_ui.py index 6f9494a..7105ee3 100644 --- a/dsst/dsst_gtk3/gtk_ui.py +++ b/dsst/dsst_gtk3/gtk_ui.py @@ -1,7 +1,7 @@ import os import gi gi.require_version('Gtk', '3.0') -from gi.repository import Gtk +from gi.repository import Gtk, GdkPixbuf from dsst_gtk3.handlers import handlers from dsst_gtk3 import util, reload, client @@ -17,6 +17,10 @@ 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) diff --git a/dsst/dsst_gtk3/resources/glade/window.glade b/dsst/dsst_gtk3/resources/glade/window.glade index 02d2131..9dc74e5 100644 --- a/dsst/dsst_gtk3/resources/glade/window.glade +++ b/dsst/dsst_gtk3/resources/glade/window.glade @@ -2505,7 +2505,7 @@ False True - 2 + 1 @@ -2519,7 +2519,7 @@ False True - 3 + 2 @@ -2531,7 +2531,7 @@ False True - 4 + 3 @@ -2542,6 +2542,18 @@ + + False + True + 4 + + + + False True diff --git a/dsst/dsst_gtk3/resources/images/dd.png b/dsst/dsst_gtk3/resources/images/dd.png new file mode 100644 index 0000000000000000000000000000000000000000..b455b2eafee9c6b64fb10891afce22d83b23e8a9 GIT binary patch literal 47632 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 gGOR-+BT9ZeoB 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 literal 0 HcmV?d00001 diff --git a/dsst/dsst_gtk3/util.py b/dsst/dsst_gtk3/util.py index 25d5142..55c480f 100644 --- a/dsst/dsst_gtk3/util.py +++ b/dsst/dsst_gtk3/util.py @@ -4,7 +4,7 @@ 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 @@ -75,7 +75,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() @@ -93,10 +93,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/func_proxy.py b/dsst/dsst_server/func_proxy.py index 754ca0d..326f1a2 100644 --- a/dsst/dsst_server/func_proxy.py +++ b/dsst/dsst_server/func_proxy.py @@ -1,5 +1,5 @@ -from dsst_server.write_functions import WriteFunctions -from dsst_server.read_functions import ReadFunctions +from dsst_server.func_write import WriteFunctions +from dsst_server.func_read import ReadFunctions class FunctionProxy(WriteFunctions, ReadFunctions): diff --git a/dsst/dsst_server/read_functions.py b/dsst/dsst_server/func_read.py similarity index 100% rename from dsst/dsst_server/read_functions.py rename to dsst/dsst_server/func_read.py diff --git a/dsst/dsst_server/write_functions.py b/dsst/dsst_server/func_write.py similarity index 100% rename from dsst/dsst_server/write_functions.py rename to dsst/dsst_server/func_write.py diff --git a/dsst/dsst_server/server.py b/dsst/dsst_server/server.py index 5a0794d..0a2793c 100644 --- a/dsst/dsst_server/server.py +++ b/dsst/dsst_server/server.py @@ -7,7 +7,7 @@ import sys import os from common import util, models -from dsst_server import read_functions, write_functions, tokens +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 @@ -31,8 +31,8 @@ class DsstServer: print('Database initialized ({})'.format(sql.db.database)) # Load access tokens and map them to their allowed methods - read_actions = util.list_class_methods(read_functions.ReadFunctions) - write_actions = util.list_class_methods(write_functions.WriteFunctions) + 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 From 703432188901dd668df61959960c347ec894ea94 Mon Sep 17 00:00:00 2001 From: luxick Date: Fri, 9 Mar 2018 20:45:12 +0100 Subject: [PATCH 06/13] Add create season function. --- dsst/dsst_gtk3/dialogs.py | 28 ++ dsst/dsst_gtk3/gtk_ui.py | 59 +-- dsst/dsst_gtk3/handlers/dialog_handlers.py | 22 +- dsst/dsst_gtk3/handlers/season_handlers.py | 13 +- dsst/dsst_gtk3/reload.py | 16 +- dsst/dsst_gtk3/resources/glade/dialogs.glade | 4 +- dsst/dsst_gtk3/resources/glade/window.glade | 373 +++++++++++++++++-- dsst/dsst_gtk3/util.py | 20 +- dsst/dsst_server/func_write.py | 14 +- dsst/dsst_server/server.py | 2 +- 10 files changed, 473 insertions(+), 78 deletions(-) diff --git a/dsst/dsst_gtk3/dialogs.py b/dsst/dsst_gtk3/dialogs.py index ded4d30..15919b5 100644 --- a/dsst/dsst_gtk3/dialogs.py +++ b/dsst/dsst_gtk3/dialogs.py @@ -1,7 +1,9 @@ """ This module contains UI functions for displaying different dialogs """ +import datetime from gi.repository import Gtk +from common import models def enter_string_dialog(builder: Gtk.Builder, title: str, value=None) -> str: @@ -28,6 +30,32 @@ def enter_string_dialog(builder: Gtk.Builder, title: str, value=None) -> str: return value +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 '') + + dialog = builder.get_object('edit_season_dialog') + result = dialog.run() + dialog.hide() + + if result != Gtk.ResponseType.OK: + return None + + 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 show_episode_dialog(builder: Gtk.Builder, title: str, season_id: int, episode): """ Shows a dialog to edit an episode :param builder: GtkBuilder with loaded 'dialogs.glade' diff --git a/dsst/dsst_gtk3/gtk_ui.py b/dsst/dsst_gtk3/gtk_ui.py index 7105ee3..d19b341 100644 --- a/dsst/dsst_gtk3/gtk_ui.py +++ b/dsst/dsst_gtk3/gtk_ui.py @@ -1,10 +1,10 @@ import os import gi gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, GdkPixbuf +from gi.repository import Gtk from dsst_gtk3.handlers import handlers from dsst_gtk3 import util, reload, client - +from common import models class GtkUi: """ The main UI class """ @@ -29,34 +29,51 @@ class GtkUi: # Connect to data server config = config['servers'][0] self.data_client = client.Access(config) - self.data = {} + # 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'))} - self.season_changed = True - self.ep_changed = False # Load base data and seasons - self.initial_load() + self.load_server_meta() + self.reload() self.update_status_bar_meta() - def initial_load(self): - with util.network_operation(self): - self.data['players'] = self.data_client.send_request('load_players') - self.data['drinks'] = self.data_client.send_request('load_drinks') - self.data['seasons'] = self.data_client.send_request('load_seasons') - self.meta['database'] = self.data_client.send_request('load_db_meta') - reload.reload_base_data(self.ui, self) + def load_server_meta(self): + self.meta['database'] = self.data_client.send_request('load_db_meta') def reload(self): - if self.season_changed: + with util.network_operation(self): + refresh_base = False + if not self.players.valid: + self.players.data = self.data_client.send_request('load_players') + refresh_base = True + if not self.drinks.valid: + self.drinks.data = self.data_client.send_request('load_drinks') + refresh_base= True + if not self.seasons.valid: + self.seasons.data = self.data_client.send_request('load_seasons') + refresh_base = True + if refresh_base: + reload.reload_base_data(self.ui, self) + + if not self.episodes.valid: with util.network_operation(self): season_id = self.get_selected_season_id() - self.data['episodes'] = self.data_client.send_request('load_episodes', season_id) - self.data['season_stats'] = self.data_client.send_request('load_season_stats', season_id) - reload.reload_episodes(self.ui, self) - reload.reload_season_stats(self) - self.season_changed = False + 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) + reload.reload_episodes(self.ui, self) + reload.reload_season_stats(self) - if self.ep_changed: - reload.reload_episode_stats(self) + 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_status_bar_meta(self): self.ui.get_object('connection_label').set_text(self.meta.get('connection')) diff --git a/dsst/dsst_gtk3/handlers/dialog_handlers.py b/dsst/dsst_gtk3/handlers/dialog_handlers.py index 1224b50..c4be4f3 100644 --- a/dsst/dsst_gtk3/handlers/dialog_handlers.py +++ b/dsst/dsst_gtk3/handlers/dialog_handlers.py @@ -1,4 +1,7 @@ -from dsst_gtk3 import dialogs, util +import datetime + +from dsst_gtk3 import dialogs, util, gtk_ui +from gi.repository import Gtk class DialogHandlers: @@ -27,3 +30,20 @@ class DialogHandlers: def do_manage_drinks(self, *_): result = dialogs.show_manage_drinks_dialog(self.app.ui) + + 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/season_handlers.py b/dsst/dsst_gtk3/handlers/season_handlers.py index 9734381..591a920 100644 --- a/dsst/dsst_gtk3/handlers/season_handlers.py +++ b/dsst/dsst_gtk3/handlers/season_handlers.py @@ -1,4 +1,4 @@ -from dsst_gtk3 import dialogs, gtk_ui +from dsst_gtk3 import dialogs, gtk_ui, reload class SeasonHandlers: @@ -7,12 +7,14 @@ 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: + season = dialogs.edit_season(self.app.ui) + if season: + self.app.update_season(season) self.app.reload() def do_season_selected(self, *_): - self.app.season_changed = True + self.app.episodes.valid = False + self.app.season_stats.valid = False self.app.reload() def do_add_episode(self, *_): @@ -23,8 +25,7 @@ class SeasonHandlers: self.app.reload() def on_selected_episode_changed(self, *_): - self.app.ep_changed = True - 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/reload.py b/dsst/dsst_gtk3/reload.py index e7d7d49..10e330c 100644 --- a/dsst/dsst_gtk3/reload.py +++ b/dsst/dsst_gtk3/reload.py @@ -10,11 +10,11 @@ def reload_base_data(builder: Gtk.Builder, app: 'gtk_ui.GtkUi',): """ # Rebuild all players store builder.get_object('all_players_store').clear() - for player in app.data['players']: + for player in app.players.data: builder.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 app.data['drinks']: + for drink in app.drinks.data: builder.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 @@ -22,7 +22,7 @@ def reload_base_data(builder: Gtk.Builder, app: 'gtk_ui.GtkUi',): with util.block_handler(combo, app.handlers.do_season_selected): store = builder.get_object('seasons_store') store.clear() - for season in app.data['seasons']: + for season in app.seasons.data: store.append([season.id, season.game_name]) combo.set_active(active) @@ -37,7 +37,7 @@ def reload_episodes(builder: Gtk.Builder, app: 'gtk_ui.GtkUi'): with util.block_handler(selection, app.handlers.on_selected_episode_changed): model, selected_paths = selection.get_selected_rows() model.clear() - for episode in app.data['episodes']: + 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]) @@ -47,7 +47,7 @@ def reload_season_stats(app: 'gtk_ui.GtkUi'): """Load statistic data for selected season :param app: GtkUi instance """ - season_stats = app.data.get('season_stats') + season_stats = app.season_stats.data # Load player kill/death data store = app.ui.get_object('player_season_store') store.clear() @@ -65,7 +65,7 @@ def reload_episode_stats(app: 'gtk_ui.GtkUi'): """Reload all data that is dependant on a selected episode :param app: app: GtkUi instance """ - episode = [ep for ep in app.data['episodes'] if ep.id == app.get_selected_episode_id()][0] + episode = [ep for ep in app.episodes.data if ep.id == app.get_selected_episode_id()][0] store = app.ui.get_object('episode_players_store') store.clear() for player in episode.players: @@ -75,7 +75,7 @@ def reload_episode_stats(app: 'gtk_ui.GtkUi'): 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]) # Reload victory store for notebook view @@ -100,7 +100,7 @@ def reload_episode_stats(app: 'gtk_ui.GtkUi'): sorted_list = Counter(enemy_list).most_common(1) if sorted_list: enemy_name, deaths = sorted_list[0] - app.ui.get_object('ep_enemy_name_label').set_text(f'{enemy_name} ({deaths} Deaths)') + app.ui.get_object('ep_enemy_name_label').set_text('{} ({} Deaths)'.format(enemy_name, deaths)) def fill_list_store(store: Gtk.ListStore, models: list): 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 9dc74e5..7c2e35e 100644 --- a/dsst/dsst_gtk3/resources/glade/window.glade +++ b/dsst/dsst_gtk3/resources/glade/window.glade @@ -1,7 +1,7 @@ - + @@ -1591,23 +1591,7 @@ - - New - True - True - True - 5 - 5 - 5 - 5 - - - - False - True - end - 1 - + @@ -1665,23 +1649,7 @@ - - New - True - True - True - 5 - 5 - 5 - 5 - - - - False - True - end - 1 - + @@ -2573,4 +2541,339 @@ + + 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 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 + + + + + diff --git a/dsst/dsst_gtk3/util.py b/dsst/dsst_gtk3/util.py index 55c480f..a535d99 100644 --- a/dsst/dsst_gtk3/util.py +++ b/dsst/dsst_gtk3/util.py @@ -16,11 +16,26 @@ DEFAULT_CONFIG = { 'host': 'localhost', 'port': 12345, 'buffer_size': 1024, - 'auth_token': 'a'} + '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 @@ -35,7 +50,7 @@ def block_handler(widget: 'Gtk.Widget', handler_func: Callable): @contextmanager def network_operation(app: 'gtk_ui.GtkUi'): """Run operation in try/except block and display exception in a dialog - :param exception: + :param app: Reference to main Gtk Application """ app.ui.get_object('status_bar').push(0, 'Connecting to server') try: @@ -47,7 +62,6 @@ def network_operation(app: 'gtk_ui.GtkUi'): 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()' diff --git a/dsst/dsst_server/func_write.py b/dsst/dsst_server/func_write.py index ea27bdf..7fe4891 100644 --- a/dsst/dsst_server/func_write.py +++ b/dsst/dsst_server/func_write.py @@ -1,8 +1,20 @@ 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_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()) diff --git a/dsst/dsst_server/server.py b/dsst/dsst_server/server.py index 0a2793c..a074838 100644 --- a/dsst/dsst_server/server.py +++ b/dsst/dsst_server/server.py @@ -62,7 +62,7 @@ class DsstServer: if action_name in self.tokens[token]: action = getattr(FunctionProxy, action_name) try: - value = action(request.get('args')) + 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)) From 437f418f49202fd0ca5d2f12a607ca980d8a06c9 Mon Sep 17 00:00:00 2001 From: luxick Date: Mon, 12 Mar 2018 20:09:04 +0100 Subject: [PATCH 07/13] Add create episode function. --- dsst/dsst_gtk3/dialogs.py | 99 +- dsst/dsst_gtk3/gtk_ui.py | 13 + dsst/dsst_gtk3/handlers/base_data_handlers.py | 4 +- dsst/dsst_gtk3/handlers/dialog_handlers.py | 14 +- dsst/dsst_gtk3/handlers/season_handlers.py | 6 +- dsst/dsst_gtk3/resources/glade/window.glade | 2177 +++++++++-------- dsst/dsst_server/func_write.py | 16 + 7 files changed, 1172 insertions(+), 1157 deletions(-) diff --git a/dsst/dsst_gtk3/dialogs.py b/dsst/dsst_gtk3/dialogs.py index 15919b5..3364eba 100644 --- a/dsst/dsst_gtk3/dialogs.py +++ b/dsst/dsst_gtk3/dialogs.py @@ -4,6 +4,7 @@ This module contains UI functions for displaying different dialogs import datetime from gi.repository import Gtk from common import models +from dsst_gtk3 import gtk_ui def enter_string_dialog(builder: Gtk.Builder, title: str, value=None) -> str: @@ -56,78 +57,44 @@ def edit_season(builder: 'Gtk.Builder', season: 'models.Season'=None): return season -def show_episode_dialog(builder: Gtk.Builder, title: str, season_id: int, episode): - """ 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 +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 """ - pass - # # 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]) - # - # 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 + 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]) -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 = 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_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() - dialog.hide() - - -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() - dialog.hide() + 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 show_edit_death_dialog(builder: Gtk.Builder, episode_id: int, death): diff --git a/dsst/dsst_gtk3/gtk_ui.py b/dsst/dsst_gtk3/gtk_ui.py index d19b341..61fd257 100644 --- a/dsst/dsst_gtk3/gtk_ui.py +++ b/dsst/dsst_gtk3/gtk_ui.py @@ -75,6 +75,12 @@ class GtkUi: 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 '') @@ -93,6 +99,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): diff --git a/dsst/dsst_gtk3/handlers/base_data_handlers.py b/dsst/dsst_gtk3/handlers/base_data_handlers.py index 5efa887..9fa56de 100644 --- a/dsst/dsst_gtk3/handlers/base_data_handlers.py +++ b/dsst/dsst_gtk3/handlers/base_data_handlers.py @@ -7,7 +7,7 @@ class BaseDataHandlers: self.app = app def do_manage_players(self, *_): - dialogs.show_manage_players_dialog(self.app.ui, 'Manage Players') + dialogs.run_management_dialog(self.app.ui, 'manage_players_dialog') def do_add_player(self, entry): if entry.get_text(): @@ -16,7 +16,7 @@ class BaseDataHandlers: self.app.reload() def do_manage_enemies(self, *_): - dialogs.show_manage_enemies_dialog(self.app.ui, self.app.get_selected_season_id()) + dialogs.run_management_dialog(self.app.ui, 'manage_enemies_dialog') def on_player_name_edited(self, _, index, value): row = self.app.ui.get_object('all_players_store')[index] diff --git a/dsst/dsst_gtk3/handlers/dialog_handlers.py b/dsst/dsst_gtk3/handlers/dialog_handlers.py index c4be4f3..a144db3 100644 --- a/dsst/dsst_gtk3/handlers/dialog_handlers.py +++ b/dsst/dsst_gtk3/handlers/dialog_handlers.py @@ -9,6 +9,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 @@ -16,10 +21,10 @@ 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]) + if not any(row[0] == player_id for row in store): + store.append([player_id, player.name, player.hex_id]) def do_add_enemy(self, entry): if entry.get_text(): @@ -28,9 +33,6 @@ class DialogHandlers: # store.append([enemy.name, False, 0, enemy.id]) entry.set_text('') - def do_manage_drinks(self, *_): - result = dialogs.show_manage_drinks_dialog(self.app.ui) - def do_show_date_picker(self, entry: 'Gtk.Entry', *_): dialog = self.app.ui.get_object('date_picker_dialog') result = dialog.run() diff --git a/dsst/dsst_gtk3/handlers/season_handlers.py b/dsst/dsst_gtk3/handlers/season_handlers.py index 591a920..e4756cf 100644 --- a/dsst/dsst_gtk3/handlers/season_handlers.py +++ b/dsst/dsst_gtk3/handlers/season_handlers.py @@ -21,8 +21,10 @@ class SeasonHandlers: 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.reload() def on_selected_episode_changed(self, *_): reload.reload_episode_stats(self.app) diff --git a/dsst/dsst_gtk3/resources/glade/window.glade b/dsst/dsst_gtk3/resources/glade/window.glade index 7c2e35e..7ae3f77 100644 --- a/dsst/dsst_gtk3/resources/glade/window.glade +++ b/dsst/dsst_gtk3/resources/glade/window.glade @@ -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 - - - - - @@ -370,157 +34,6 @@ - - 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 @@ -548,312 +61,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 @@ -1088,291 +295,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 +348,15 @@ + + + True + False + Add Episode + True + + + True @@ -1438,7 +369,7 @@ False Manage Enemies True - + @@ -1510,7 +441,7 @@ False Manage Players True - + @@ -1519,7 +450,7 @@ False Manage Drinks True - + @@ -2655,6 +1586,600 @@ + + 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 + 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 + + + + + + + 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 @@ -2876,4 +2401,494 @@ + + 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 + + + + 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 + + + + + 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 + + + + + + + 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 + + + + + diff --git a/dsst/dsst_server/func_write.py b/dsst/dsst_server/func_write.py index 7fe4891..7545d25 100644 --- a/dsst/dsst_server/func_write.py +++ b/dsst/dsst_server/func_write.py @@ -18,3 +18,19 @@ class WriteFunctions: 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() From c3e6793f6905f0f2d674bff2a6d8d3bd6aed7e29 Mon Sep 17 00:00:00 2001 From: luxick Date: Wed, 14 Mar 2018 13:40:02 +0100 Subject: [PATCH 08/13] Create and modify enemies function. --- dsst/common/models.py | 1 + dsst/dsst_gtk3/dialogs.py | 107 +-- dsst/dsst_gtk3/gtk_ui.py | 41 +- dsst/dsst_gtk3/handlers/base_data_handlers.py | 20 +- dsst/dsst_gtk3/handlers/death_handlers.py | 7 +- dsst/dsst_gtk3/handlers/dialog_handlers.py | 25 +- dsst/dsst_gtk3/handlers/season_handlers.py | 6 +- dsst/dsst_gtk3/handlers/victory_handlers.py | 2 +- dsst/dsst_gtk3/reload.py | 38 +- dsst/dsst_gtk3/resources/glade/window.glade | 709 +++++++++++------- dsst/dsst_server/func_read.py | 7 +- dsst/dsst_server/func_write.py | 9 + 12 files changed, 582 insertions(+), 390 deletions(-) diff --git a/dsst/common/models.py b/dsst/common/models.py index 9e29bd7..09958a0 100644 --- a/dsst/common/models.py +++ b/dsst/common/models.py @@ -57,6 +57,7 @@ class Death: self.enemy = arg.get('enemy') self.episode = arg.get('episode') self.penalties = arg.get('penalties') + self.time = arg.get('time') class Penalty: diff --git a/dsst/dsst_gtk3/dialogs.py b/dsst/dsst_gtk3/dialogs.py index 3364eba..4642f74 100644 --- a/dsst/dsst_gtk3/dialogs.py +++ b/dsst/dsst_gtk3/dialogs.py @@ -4,7 +4,7 @@ This module contains UI functions for displaying different dialogs import datetime from gi.repository import Gtk from common import models -from dsst_gtk3 import gtk_ui +from dsst_gtk3 import gtk_ui, util def enter_string_dialog(builder: Gtk.Builder, title: str, value=None) -> str: @@ -97,49 +97,68 @@ def edit_episode(app: 'gtk_ui.GtkUi', season_id: int, episode: 'models.Episode'= return episode -def show_edit_death_dialog(builder: Gtk.Builder, episode_id: int, death): - pass - # """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 - # """ - # 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) - # - # # 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.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 edit_death(app: 'gtk_ui.GtkUi', death: 'models.Death'=None): + """Show a dialog to create or edit death events for an episode + :param app: Main Gtk application + :param death: (Optional) Existing death object to edit + :return: Death object or None if dialog was canceled + """ + if not death: + death = models.Death() + death.episode = app.get_selected_episode_id() + death.info = "" + death.penalties = [] + death.time = datetime.time(0, 0) + hour_spin = app.ui.get_object('death_hour_spin') + min_spin = app.ui.get_object('death_min_spin') + # Set time of death + hour_spin.set_value(death.time.hour) + min_spin.set_value(death.time.minute) + # Set Enemy + if death.enemy: + index = util.get_index_of_combo_model(app.ui.get_object('edit_death_enemy_combo'), 0, death.enemy.id) + app.ui.get_object('edit_death_enemy_combo').set_active(index) + # Set player + if death.player: + index = util.get_index_of_combo_model(app.ui.get_object('edit_death_player_combo'), 0, death.player.id) + app.ui.get_object('edit_death_player_combo').set_active(index) + # Set shot size + if death.penalties: + app.ui.get_object('edit_death_size_spin').set_value(death.penalties[0].size) + # Set info comment + app.ui.get_object('edit_death_comment_entry').set_text(death.info) + # Set penalties + default_drink = app.drinks.data[0].name + store = app.ui.get_object('player_penalties_store') + store.clear() + if death.penalties: + for penalty in death.penalties: + store.append([penalty.id, penalty.player.name, penalty.drink.name, penalty.player.id]) + else: + for player in app.ui.get_object('episode_players_store'): + store.append([None, player[1], default_drink, player[0]]) + + # 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 + + # Parse the inputs + death.time = datetime.time(hour_spin.get_value(), min_spin.set_value) + death.enemy = util.get_combo_value(app.ui.get_object('edit_death_enemy_combo'), 3) + 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() + store = app.ui.get_object('player_penalties_store') + size = app.ui.get_object('edit_death_size_spin').get_value() + death.penalties.clear() + 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_victory_dialog(builder: Gtk.Builder, episode_id: int, victory): diff --git a/dsst/dsst_gtk3/gtk_ui.py b/dsst/dsst_gtk3/gtk_ui.py index 61fd257..a54070a 100644 --- a/dsst/dsst_gtk3/gtk_ui.py +++ b/dsst/dsst_gtk3/gtk_ui.py @@ -40,35 +40,32 @@ class GtkUi: self.meta = {'connection': '{}:{}'.format(config.get('host'), config.get('port'))} # Load base data and seasons self.load_server_meta() - self.reload() + 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 reload(self): + def full_reload(self): with util.network_operation(self): - refresh_base = False - if not self.players.valid: - self.players.data = self.data_client.send_request('load_players') - refresh_base = True - if not self.drinks.valid: - self.drinks.data = self.data_client.send_request('load_drinks') - refresh_base= True - if not self.seasons.valid: - self.seasons.data = self.data_client.send_request('load_seasons') - refresh_base = True - if refresh_base: - reload.reload_base_data(self.ui, 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) - if not self.episodes.valid: - with util.network_operation(self): - 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) - reload.reload_episodes(self.ui, self) - reload.reload_season_stats(self) + def reload(self): + pass + + 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_season(self, season: 'models.Season'): with util.network_operation(self): diff --git a/dsst/dsst_gtk3/handlers/base_data_handlers.py b/dsst/dsst_gtk3/handlers/base_data_handlers.py index 9fa56de..eade0e0 100644 --- a/dsst/dsst_gtk3/handlers/base_data_handlers.py +++ b/dsst/dsst_gtk3/handlers/base_data_handlers.py @@ -1,4 +1,4 @@ -from dsst_gtk3 import dialogs +from dsst_gtk3 import dialogs, gtk_ui class BaseDataHandlers: @@ -6,48 +6,42 @@ class BaseDataHandlers: def __init__(self, app: 'gtk_ui.GtkUi'): self.app = app - def do_manage_players(self, *_): - dialogs.run_management_dialog(self.app.ui, 'manage_players_dialog') - def do_add_player(self, entry): if entry.get_text(): # sql.Player.create(name=entry.get_text()) entry.set_text('') - self.app.reload() - - def do_manage_enemies(self, *_): - dialogs.run_management_dialog(self.app.ui, 'manage_enemies_dialog') + self.app.full_reload() 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() + self.app.full_reload() 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() + self.app.full_reload() def do_add_drink(self, entry): if entry.get_text(): sql.Drink.create(name=entry.get_text(), vol=0) entry.set_text('') - self.app.reload() + self.app.full_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() + self.app.full_reload() 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 + self.app.full_reload() \ 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 410b3a0..3594afd 100644 --- a/dsst/dsst_gtk3/handlers/death_handlers.py +++ b/dsst/dsst_gtk3/handlers/death_handlers.py @@ -1,5 +1,5 @@ from gi.repository import Gtk -from dsst_gtk3 import dialogs +from dsst_gtk3 import dialogs, gtk_ui class DeathHandlers: @@ -11,9 +11,10 @@ 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) + result = dialogs.edit_death(self.app) if result == Gtk.ResponseType.OK: - self.app.reload() + self.app.episodes.valid = False + self.app.full_reload() 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 a144db3..14ca4ae 100644 --- a/dsst/dsst_gtk3/handlers/dialog_handlers.py +++ b/dsst/dsst_gtk3/handlers/dialog_handlers.py @@ -1,6 +1,7 @@ import datetime -from dsst_gtk3 import dialogs, util, gtk_ui +from dsst_gtk3 import dialogs, util, gtk_ui, reload +from common import models from gi.repository import Gtk @@ -29,10 +30,28 @@ 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('') + 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() diff --git a/dsst/dsst_gtk3/handlers/season_handlers.py b/dsst/dsst_gtk3/handlers/season_handlers.py index e4756cf..c213bb1 100644 --- a/dsst/dsst_gtk3/handlers/season_handlers.py +++ b/dsst/dsst_gtk3/handlers/season_handlers.py @@ -10,12 +10,12 @@ class SeasonHandlers: season = dialogs.edit_season(self.app.ui) if season: self.app.update_season(season) - self.app.reload() + self.app.full_reload() def do_season_selected(self, *_): self.app.episodes.valid = False self.app.season_stats.valid = False - self.app.reload() + self.app.full_reload() def do_add_episode(self, *_): season_id = self.app.get_selected_season_id() @@ -24,7 +24,7 @@ class SeasonHandlers: ep = dialogs.edit_episode(self.app, season_id) if ep: self.app.update_episode(ep) - self.app.reload() + self.app.full_reload() def on_selected_episode_changed(self, *_): reload.reload_episode_stats(self.app) diff --git a/dsst/dsst_gtk3/handlers/victory_handlers.py b/dsst/dsst_gtk3/handlers/victory_handlers.py index 8b1f9a5..6359e2f 100644 --- a/dsst/dsst_gtk3/handlers/victory_handlers.py +++ b/dsst/dsst_gtk3/handlers/victory_handlers.py @@ -13,4 +13,4 @@ class VictoryHandlers: return result = dialogs.show_edit_victory_dialog(self.app.ui, ep_id) if result == Gtk.ResponseType.OK: - self.app.reload() + self.app.full_reload() diff --git a/dsst/dsst_gtk3/reload.py b/dsst/dsst_gtk3/reload.py index 10e330c..89bf92c 100644 --- a/dsst/dsst_gtk3/reload.py +++ b/dsst/dsst_gtk3/reload.py @@ -3,37 +3,38 @@ from gi.repository import Gtk 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() + app.ui.get_object('all_players_store').clear() for player in app.players.data: - builder.get_object('all_players_store').append([player.id, player.name, player.hex_id]) + app.ui.get_object('all_players_store').append([player.id, player.name, player.hex_id]) # Rebuild drink store - builder.get_object('drink_store').clear() + app.ui.get_object('drink_store').clear() for drink in app.drinks.data: - builder.get_object('drink_store').append([drink.id, drink.name, '{:.2f}%'.format(drink.vol)]) + 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 app.seasons.data: store.append([season.id, season.game_name]) combo.set_active(active) -def reload_episodes(builder: Gtk.Builder, app: 'gtk_ui.GtkUi'): +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 """ # 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() @@ -47,6 +48,7 @@ def reload_season_stats(app: 'gtk_ui.GtkUi'): """Load statistic data for selected season :param app: GtkUi instance """ + 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') @@ -57,15 +59,17 @@ def reload_season_stats(app: 'gtk_ui.GtkUi'): # Load enemy stats for season store = app.ui.get_object('enemy_season_store') store.clear() - for enemy_name, deaths, defeated, boss in season_stats.enemies: - store.append([enemy_name, defeated, deaths, boss]) + 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(app: 'gtk_ui.GtkUi'): """Reload all data that is dependant on a selected episode :param app: app: GtkUi instance """ - episode = [ep for ep in app.episodes.data if ep.id == app.get_selected_episode_id()][0] + 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: @@ -106,4 +110,12 @@ def reload_episode_stats(app: 'gtk_ui.GtkUi'): def fill_list_store(store: Gtk.ListStore, models: list): store.clear() for model in models: - pass \ No newline at end of file + 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/window.glade b/dsst/dsst_gtk3/resources/glade/window.glade index 7ae3f77..aede1f1 100644 --- a/dsst/dsst_gtk3/resources/glade/window.glade +++ b/dsst/dsst_gtk3/resources/glade/window.glade @@ -32,6 +32,8 @@ + + @@ -283,6 +285,16 @@ + + 24 + 1 + 1 + + + 60 + 1 + 1 + @@ -1586,292 +1598,6 @@ - - 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 - 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 - - - - - False Edit Episode @@ -2649,6 +2375,20 @@ 1 + + + Optional + True + True + False + True + + + False + True + 2 + + False @@ -2689,14 +2429,31 @@ Name + True - + + True + + 0 + + + Boss + + + + + + 3 + + + + @@ -2891,4 +2648,382 @@ + + 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 + + + + + True + 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_server/func_read.py b/dsst/dsst_server/func_read.py index 6a95f82..40afa44 100644 --- a/dsst/dsst_server/func_read.py +++ b/dsst/dsst_server/func_read.py @@ -26,6 +26,10 @@ class ReadFunctions: 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()] @@ -39,7 +43,8 @@ class ReadFunctions: 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.name, + model.enemies = [(enemy.id, + enemy.name, sql_func.enemy_attempts(enemy.id), sql.Victory.select().where(sql.Victory.enemy == enemy.id).exists(), enemy.boss) diff --git a/dsst/dsst_server/func_write.py b/dsst/dsst_server/func_write.py index 7545d25..925f52d 100644 --- a/dsst/dsst_server/func_write.py +++ b/dsst/dsst_server/func_write.py @@ -7,6 +7,15 @@ class WriteFunctions: 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_season(season: 'models.Season', *_): (sql.Season From ec71d4415f0682dc3a5840d99d3eeddd731891be Mon Sep 17 00:00:00 2001 From: luxick Date: Wed, 14 Mar 2018 13:56:11 +0100 Subject: [PATCH 09/13] Create and modify players function. --- dsst/dsst_gtk3/gtk_ui.py | 5 +++++ dsst/dsst_gtk3/handlers/base_data_handlers.py | 20 +++++++++---------- dsst/dsst_server/func_write.py | 10 +++++++++- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/dsst/dsst_gtk3/gtk_ui.py b/dsst/dsst_gtk3/gtk_ui.py index a54070a..25c9dff 100644 --- a/dsst/dsst_gtk3/gtk_ui.py +++ b/dsst/dsst_gtk3/gtk_ui.py @@ -67,6 +67,11 @@ class GtkUi: 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_season(self, season: 'models.Season'): with util.network_operation(self): self.data_client.send_request('update_season', season) diff --git a/dsst/dsst_gtk3/handlers/base_data_handlers.py b/dsst/dsst_gtk3/handlers/base_data_handlers.py index eade0e0..2c431b1 100644 --- a/dsst/dsst_gtk3/handlers/base_data_handlers.py +++ b/dsst/dsst_gtk3/handlers/base_data_handlers.py @@ -1,4 +1,5 @@ from dsst_gtk3 import dialogs, gtk_ui +from common import models class BaseDataHandlers: @@ -8,23 +9,22 @@ class BaseDataHandlers: 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.full_reload() 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.full_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.full_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(): diff --git a/dsst/dsst_server/func_write.py b/dsst/dsst_server/func_write.py index 925f52d..e1b12fd 100644 --- a/dsst/dsst_server/func_write.py +++ b/dsst/dsst_server/func_write.py @@ -8,7 +8,7 @@ class WriteFunctions: return 'Season created.' @staticmethod - def update_enemy(enemy: 'models.Enemy'): + 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, @@ -16,6 +16,14 @@ class WriteFunctions: 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_season(season: 'models.Season', *_): (sql.Season From ac01a4441b2e95902e13a1788dc99b6032c62c63 Mon Sep 17 00:00:00 2001 From: luxick Date: Wed, 14 Mar 2018 14:08:19 +0100 Subject: [PATCH 10/13] Create and modify drinks function. --- dsst/dsst_gtk3/gtk_ui.py | 5 +++++ dsst/dsst_gtk3/handlers/base_data_handlers.py | 18 ++++++++---------- dsst/dsst_gtk3/resources/glade/window.glade | 1 + dsst/dsst_server/func_write.py | 8 ++++++++ 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/dsst/dsst_gtk3/gtk_ui.py b/dsst/dsst_gtk3/gtk_ui.py index 25c9dff..5f09263 100644 --- a/dsst/dsst_gtk3/gtk_ui.py +++ b/dsst/dsst_gtk3/gtk_ui.py @@ -72,6 +72,11 @@ class GtkUi: 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 update_season(self, season: 'models.Season'): with util.network_operation(self): self.data_client.send_request('update_season', season) diff --git a/dsst/dsst_gtk3/handlers/base_data_handlers.py b/dsst/dsst_gtk3/handlers/base_data_handlers.py index 2c431b1..15738a1 100644 --- a/dsst/dsst_gtk3/handlers/base_data_handlers.py +++ b/dsst/dsst_gtk3/handlers/base_data_handlers.py @@ -28,20 +28,18 @@ class BaseDataHandlers: 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.full_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.full_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.full_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/resources/glade/window.glade b/dsst/dsst_gtk3/resources/glade/window.glade index aede1f1..23f1034 100644 --- a/dsst/dsst_gtk3/resources/glade/window.glade +++ b/dsst/dsst_gtk3/resources/glade/window.glade @@ -2246,6 +2246,7 @@ Name + True True diff --git a/dsst/dsst_server/func_write.py b/dsst/dsst_server/func_write.py index e1b12fd..7de237b 100644 --- a/dsst/dsst_server/func_write.py +++ b/dsst/dsst_server/func_write.py @@ -24,6 +24,14 @@ class WriteFunctions: 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 update_season(season: 'models.Season', *_): (sql.Season From 48ed0d830fc71bde7bce823e598b7813dbad5d56 Mon Sep 17 00:00:00 2001 From: luxick Date: Wed, 14 Mar 2018 14:47:04 +0100 Subject: [PATCH 11/13] Create death events function. --- dsst/dsst_gtk3/dialogs.py | 4 ++-- dsst/dsst_gtk3/gtk_ui.py | 5 +++++ dsst/dsst_gtk3/handlers/death_handlers.py | 7 +++---- dsst/dsst_gtk3/reload.py | 3 ++- dsst/dsst_gtk3/resources/glade/window.glade | 13 +++++++++++++ dsst/dsst_server/data_access/sql.py | 3 +++ dsst/dsst_server/func_write.py | 10 ++++++++++ 7 files changed, 38 insertions(+), 7 deletions(-) diff --git a/dsst/dsst_gtk3/dialogs.py b/dsst/dsst_gtk3/dialogs.py index 4642f74..26f4702 100644 --- a/dsst/dsst_gtk3/dialogs.py +++ b/dsst/dsst_gtk3/dialogs.py @@ -146,8 +146,8 @@ def edit_death(app: 'gtk_ui.GtkUi', death: 'models.Death'=None): return None # Parse the inputs - death.time = datetime.time(hour_spin.get_value(), min_spin.set_value) - death.enemy = util.get_combo_value(app.ui.get_object('edit_death_enemy_combo'), 3) + 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() store = app.ui.get_object('player_penalties_store') diff --git a/dsst/dsst_gtk3/gtk_ui.py b/dsst/dsst_gtk3/gtk_ui.py index 5f09263..e8e3408 100644 --- a/dsst/dsst_gtk3/gtk_ui.py +++ b/dsst/dsst_gtk3/gtk_ui.py @@ -77,6 +77,11 @@ class GtkUi: 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 update_season(self, season: 'models.Season'): with util.network_operation(self): self.data_client.send_request('update_season', season) diff --git a/dsst/dsst_gtk3/handlers/death_handlers.py b/dsst/dsst_gtk3/handlers/death_handlers.py index 3594afd..50f7f30 100644 --- a/dsst/dsst_gtk3/handlers/death_handlers.py +++ b/dsst/dsst_gtk3/handlers/death_handlers.py @@ -11,10 +11,9 @@ class DeathHandlers: ep_id = self.app.get_selected_episode_id() if not ep_id: return - result = dialogs.edit_death(self.app) - if result == Gtk.ResponseType.OK: - self.app.episodes.valid = False - self.app.full_reload() + death = dialogs.edit_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/reload.py b/dsst/dsst_gtk3/reload.py index 89bf92c..98c01e5 100644 --- a/dsst/dsst_gtk3/reload.py +++ b/dsst/dsst_gtk3/reload.py @@ -81,7 +81,8 @@ def reload_episode_stats(app: 'gtk_ui.GtkUi'): penalties = [x.drink.name for x in death.penalties] 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 = '{}:{}'.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 = app.ui.get_object('episode_victories_store') store.clear() diff --git a/dsst/dsst_gtk3/resources/glade/window.glade b/dsst/dsst_gtk3/resources/glade/window.glade index 23f1034..17e3648 100644 --- a/dsst/dsst_gtk3/resources/glade/window.glade +++ b/dsst/dsst_gtk3/resources/glade/window.glade @@ -51,6 +51,8 @@ + + @@ -987,6 +989,17 @@ + + + Time + + + + 4 + + + + Player diff --git a/dsst/dsst_server/data_access/sql.py b/dsst/dsst_server/data_access/sql.py index cd9391c..c5ae0f6 100644 --- a/dsst/dsst_server/data_access/sql.py +++ b/dsst/dsst_server/data_access/sql.py @@ -6,6 +6,8 @@ from sql import Episode query = Episode.select().where(Episode.name == 'MyName') """ import sys +import datetime + try: from peewee import * except ImportError: @@ -70,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') diff --git a/dsst/dsst_server/func_write.py b/dsst/dsst_server/func_write.py index 7de237b..a74aacb 100644 --- a/dsst/dsst_server/func_write.py +++ b/dsst/dsst_server/func_write.py @@ -32,6 +32,16 @@ class WriteFunctions: 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 update_season(season: 'models.Season', *_): (sql.Season From 5c346fb892cd887d361966749c7a5e83c30d059c Mon Sep 17 00:00:00 2001 From: luxick Date: Wed, 14 Mar 2018 19:33:21 +0100 Subject: [PATCH 12/13] Create victory events function. --- dsst/common/models.py | 1 + dsst/dsst_gtk3/dialogs.py | 107 ++--- dsst/dsst_gtk3/gtk_ui.py | 13 +- dsst/dsst_gtk3/handlers/death_handlers.py | 2 +- dsst/dsst_gtk3/handlers/victory_handlers.py | 8 +- dsst/dsst_gtk3/reload.py | 5 +- dsst/dsst_gtk3/resources/glade/window.glade | 497 ++++++++++++-------- dsst/dsst_server/data_access/sql.py | 1 + dsst/dsst_server/func_write.py | 7 + 9 files changed, 352 insertions(+), 289 deletions(-) diff --git a/dsst/common/models.py b/dsst/common/models.py index 09958a0..55dd14c 100644 --- a/dsst/common/models.py +++ b/dsst/common/models.py @@ -76,6 +76,7 @@ class Victory: self.player = arg.get('player') self.enemy = arg.get('enemy') self.episode = arg.get('episode') + self.time = arg.get('time') class SeasonStats: diff --git a/dsst/dsst_gtk3/dialogs.py b/dsst/dsst_gtk3/dialogs.py index 26f4702..3986e1c 100644 --- a/dsst/dsst_gtk3/dialogs.py +++ b/dsst/dsst_gtk3/dialogs.py @@ -97,47 +97,11 @@ def edit_episode(app: 'gtk_ui.GtkUi', season_id: int, episode: 'models.Episode'= return episode -def edit_death(app: 'gtk_ui.GtkUi', death: 'models.Death'=None): - """Show a dialog to create or edit death events for an episode +def create_death(app: 'gtk_ui.GtkUi'): + """Show a dialog to create death events for an episode :param app: Main Gtk application - :param death: (Optional) Existing death object to edit :return: Death object or None if dialog was canceled """ - if not death: - death = models.Death() - death.episode = app.get_selected_episode_id() - death.info = "" - death.penalties = [] - death.time = datetime.time(0, 0) - hour_spin = app.ui.get_object('death_hour_spin') - min_spin = app.ui.get_object('death_min_spin') - # Set time of death - hour_spin.set_value(death.time.hour) - min_spin.set_value(death.time.minute) - # Set Enemy - if death.enemy: - index = util.get_index_of_combo_model(app.ui.get_object('edit_death_enemy_combo'), 0, death.enemy.id) - app.ui.get_object('edit_death_enemy_combo').set_active(index) - # Set player - if death.player: - index = util.get_index_of_combo_model(app.ui.get_object('edit_death_player_combo'), 0, death.player.id) - app.ui.get_object('edit_death_player_combo').set_active(index) - # Set shot size - if death.penalties: - app.ui.get_object('edit_death_size_spin').set_value(death.penalties[0].size) - # Set info comment - app.ui.get_object('edit_death_comment_entry').set_text(death.info) - # Set penalties - default_drink = app.drinks.data[0].name - store = app.ui.get_object('player_penalties_store') - store.clear() - if death.penalties: - for penalty in death.penalties: - store.append([penalty.id, penalty.player.name, penalty.drink.name, penalty.player.id]) - else: - for player in app.ui.get_object('episode_players_store'): - store.append([None, player[1], default_drink, player[0]]) - # Run the dialog dialog = app.ui.get_object("edit_death_dialog") # type: Gtk.Dialog result = dialog.run() @@ -145,14 +109,18 @@ def edit_death(app: 'gtk_ui.GtkUi', death: 'models.Death'=None): 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.clear() + 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]}) @@ -161,43 +129,24 @@ def edit_death(app: 'gtk_ui.GtkUi', death: 'models.Death'=None): return death -def show_edit_victory_dialog(builder: Gtk.Builder, episode_id: int, victory): - pass - # """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 +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 = app.ui.get_object('edit_victory_dialog') + result = dialog.run() + dialog.hide() + if result != Gtk.ResponseType.OK: + return None + + 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())) + + return victory diff --git a/dsst/dsst_gtk3/gtk_ui.py b/dsst/dsst_gtk3/gtk_ui.py index e8e3408..abd54b9 100644 --- a/dsst/dsst_gtk3/gtk_ui.py +++ b/dsst/dsst_gtk3/gtk_ui.py @@ -65,22 +65,27 @@ class GtkUi: def update_enemy(self, enemy: 'models.Enemy'): with util.network_operation(self): self.data_client.send_request('update_enemy', enemy) - self.full_reload() + 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() + 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() + 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() + 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): diff --git a/dsst/dsst_gtk3/handlers/death_handlers.py b/dsst/dsst_gtk3/handlers/death_handlers.py index 50f7f30..0522c4e 100644 --- a/dsst/dsst_gtk3/handlers/death_handlers.py +++ b/dsst/dsst_gtk3/handlers/death_handlers.py @@ -11,7 +11,7 @@ class DeathHandlers: ep_id = self.app.get_selected_episode_id() if not ep_id: return - death = dialogs.edit_death(self.app) + death = dialogs.create_death(self.app) if death: self.app.save_death(death) diff --git a/dsst/dsst_gtk3/handlers/victory_handlers.py b/dsst/dsst_gtk3/handlers/victory_handlers.py index 6359e2f..3b47122 100644 --- a/dsst/dsst_gtk3/handlers/victory_handlers.py +++ b/dsst/dsst_gtk3/handlers/victory_handlers.py @@ -1,5 +1,5 @@ from gi.repository import Gtk -from dsst_gtk3 import dialogs +from dsst_gtk3 import dialogs, gtk_ui class VictoryHandlers: @@ -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.full_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 98c01e5..bc1d386 100644 --- a/dsst/dsst_gtk3/reload.py +++ b/dsst/dsst_gtk3/reload.py @@ -81,13 +81,14 @@ def reload_episode_stats(app: 'gtk_ui.GtkUi'): penalties = [x.drink.name for x in death.penalties] penalties = ['{}x {}'.format(number, drink) for drink, number in Counter(penalties).items()] penalty_string = ', '.join(penalties) - time_string = '{}:{}'.format(death.time.hour, death.time.minute) + 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 = 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 app.ui.get_object('ep_stat_title').set_text('Stats for episode {}\n{}'.format(episode.number, episode.name)) diff --git a/dsst/dsst_gtk3/resources/glade/window.glade b/dsst/dsst_gtk3/resources/glade/window.glade index 17e3648..c058222 100644 --- a/dsst/dsst_gtk3/resources/glade/window.glade +++ b/dsst/dsst_gtk3/resources/glade/window.glade @@ -65,204 +65,6 @@ - - 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 - - - - - @@ -273,6 +75,8 @@ + + @@ -613,6 +417,7 @@ True True episodes_store + False 0 @@ -1069,6 +874,17 @@ + + + Time + + + + 4 + + + + Player @@ -2140,6 +1956,289 @@ + + 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 @@ -2794,7 +2893,7 @@ - True + False True end 1 diff --git a/dsst/dsst_server/data_access/sql.py b/dsst/dsst_server/data_access/sql.py index c5ae0f6..90949d9 100644 --- a/dsst/dsst_server/data_access/sql.py +++ b/dsst/dsst_server/data_access/sql.py @@ -95,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_server/func_write.py b/dsst/dsst_server/func_write.py index a74aacb..5aebaa4 100644 --- a/dsst/dsst_server/func_write.py +++ b/dsst/dsst_server/func_write.py @@ -42,6 +42,13 @@ class WriteFunctions: 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 From b012d24c554318d630d4a52271c172f281fe9e73 Mon Sep 17 00:00:00 2001 From: luxick Date: Wed, 14 Mar 2018 19:44:28 +0100 Subject: [PATCH 13/13] Update README --- README.md | 30 ++++++++++++++++++++++-------- build.py | 1 + 2 files changed, 23 insertions(+), 8 deletions(-) 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 3b2ad30..cf1a6fa 100644 --- a/build.py +++ b/build.py @@ -49,6 +49,7 @@ def build_server(): def build_gtk3(): build('dsst-gtk3-{}'.format(CLIENT_VERSION), 'dsst_gtk3', 'dsst_gtk3.gtk_ui:main') + build_modes = { 'server': build_server, 'gtk3': build_gtk3