From cb8646bae2c1ccaa5daa4a415d572f7199afaa04 Mon Sep 17 00:00:00 2001 From: luxick Date: Sat, 5 May 2018 15:52:12 +0200 Subject: [PATCH] Delete functions on client. --- build.py | 4 +- dsst/dsst_gtk3/client.py | 156 +++++++++++++++++- dsst/dsst_gtk3/gtk_ui.py | 39 +---- dsst/dsst_gtk3/handlers/base_data_handlers.py | 12 +- dsst/dsst_gtk3/handlers/death_handlers.py | 15 +- dsst/dsst_gtk3/handlers/dialog_handlers.py | 6 +- dsst/dsst_gtk3/handlers/season_handlers.py | 4 +- dsst/dsst_gtk3/handlers/victory_handlers.py | 14 +- dsst/dsst_gtk3/reload.py | 2 +- dsst/dsst_gtk3/resources/glade/window.glade | 102 ++++++++---- dsst/dsst_gtk3/util.py | 25 +++ dsst/dsst_server/data_access/sql.py | 14 +- dsst/dsst_server/func_delete.py | 15 ++ dsst/dsst_server/func_proxy.py | 3 +- 14 files changed, 311 insertions(+), 100 deletions(-) create mode 100644 dsst/dsst_server/func_delete.py diff --git a/build.py b/build.py index cf1a6fa..05ff359 100644 --- a/build.py +++ b/build.py @@ -9,8 +9,8 @@ import shutil INTERPRETER = '/usr/bin/env python3' -CLIENT_VERSION = '0.1' -SERVER_VERSION = '0.1' +CLIENT_VERSION = '0.2' +SERVER_VERSION = '0.2' try: build_mode = sys.argv[1] diff --git a/dsst/dsst_gtk3/client.py b/dsst/dsst_gtk3/client.py index 8c39cee..205fc2e 100644 --- a/dsst/dsst_gtk3/client.py +++ b/dsst/dsst_gtk3/client.py @@ -5,10 +5,26 @@ try: import cPickle as pickle except ImportError: import pickle +from dsst_gtk3 import gtk_ui -class Access: - def __init__(self, conn_dict): +def gui_handled(func): + def wrapper(*args, **kwargs): + self.app.ui.get_object('status_bar').push(0, 'Connecting to server') + try: + yield func(*args, **kwargs) + except Exception as e: + print(e) + self.app.ui.get_object('status_bar').push(0, str(e)) + else: + self.app.ui.get_object('status_bar').push(0, '') + self.app.full_reload() + + return wrapper + +class DataClient: + def __init__(self, app: 'gtk_ui.GtkUi', conn_dict): + self.app = app self.host = conn_dict.get('host') self.port = conn_dict.get('port') self.buffer = conn_dict.get('buffer_size') @@ -32,10 +48,144 @@ class Access: return message.get('data') + + @gui_handled + def update_enemy(self, enemy: 'models.Enemy'): + self.send_request('update_enemy', enemy) + + def update_player(self, player: 'models.Player'): + self.send_request('update_player', player) + + def update_drink(self, drink: 'models.Drink'): + self.send_request('update_drink', drink) + + def save_death(self, death: 'models.Death'): + self.send_request('save_death', death) + + def delete_death(self, death_id: int): + with util.network_operation(self): + self.data_client.send_request('delete_death', death_id) + 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 delete_victory(self, victory_id: int): + with util.network_operation(self): + self.data_client.send_request('delete_victory', victory_id) + self.full_reload() + + def update_season(self, season: 'models.Season'): + with util.network_operation(self): + self.data_client.send_request('update_season', season) + self.seasons.valid = False + + def update_episode(self, episode: 'models.Episode'): + with util.network_operation(self): + self.data_client.send_request('update_episode', episode) + self.episodes.valid = False + self.season_stats.valid = False + if __name__ == '__main__': - access = Access({'host': 'europa', 'port': 12345, 'buffer_size': 1024, 'auth_token': 'a'}) + access = DataClient({'host': 'europa', 'port': 12345, 'buffer_size': 1024, 'auth_token': 'a'}) action = 'load_seasons' response = access.send_request(action) pp = pprint.PrettyPrinter(indent=1) for s in response: pp.pprint(s.__dict__) +import pprint +import socket +from common import util, models +from functools import wraps +try: + import cPickle as pickle +except ImportError: + import pickle +from dsst_gtk3 import gtk_ui + + +def reload_after_update(method): + """ Method decorator to handle GUI while updating data. + Adds error display in case of an exception and causes data reloading after an successful change. + :param method: The method to decorate + :return: The decorated function + """ + @wraps(method) + def wrapper(self, *args, **kwargs): + # Set info in statusbar + self.app.ui.get_object('status_bar').push(0, 'Connecting to server') + try: + method(self, *args, **kwargs) + except Exception as e: + print(e) + self.app.ui.get_object('status_bar').push(0, str(e)) + else: + self.app.ui.get_object('status_bar').push(0, '') + # Cause local data to be reloaded, if method was executed successfully + self.app.full_reload() + return wrapper + + +class DataClient: + """ The access class for reading and writing data from and to the dsst API """ + def __init__(self, app: 'gtk_ui.GtkUi', conn_dict): + self.app = app + self.host = conn_dict.get('host') + self.port = conn_dict.get('port') + self.buffer = conn_dict.get('buffer_size') + self.auth_token = conn_dict.get('auth_token') + + def send_request(self, action: str, *args): + request = {'auth_token': self.auth_token, + 'action': action, + 'args': args} + request = pickle.dumps(request) + soc = socket.socket() + try: + soc.connect((self.host, self.port)) + util.send_msg(soc, request) + message = util.recv_msg(soc) + message = pickle.loads(message) + if not message.get('success'): + raise Exception(message.get('message')) + finally: + soc.close() + return message.get('data') + + @reload_after_update + def update_enemy(self, enemy: 'models.Enemy'): + self.send_request('update_enemy', enemy) + + @reload_after_update + def update_player(self, player: 'models.Player'): + self.send_request('update_player', player) + + @reload_after_update + def update_drink(self, drink: 'models.Drink'): + self.send_request('update_drink', drink) + + @reload_after_update + def save_death(self, death: 'models.Death'): + self.send_request('save_death', death) + + @reload_after_update + def delete_death(self, death_id: int): + self.send_request('delete_death', death_id) + + @reload_after_update + def save_victory(self, victory: 'models.Victory'): + self.send_request('save_victory', victory) + + @reload_after_update + def delete_victory(self, victory_id: int): + self.send_request('delete_victory', victory_id) + + @reload_after_update + def update_season(self, season: 'models.Season'): + self.send_request('update_season', season) + + @reload_after_update + def update_episode(self, episode: 'models.Episode'): + self.send_request('update_episode', episode) diff --git a/dsst/dsst_gtk3/gtk_ui.py b/dsst/dsst_gtk3/gtk_ui.py index abd54b9..83e763e 100644 --- a/dsst/dsst_gtk3/gtk_ui.py +++ b/dsst/dsst_gtk3/gtk_ui.py @@ -6,6 +6,7 @@ from dsst_gtk3.handlers import handlers from dsst_gtk3 import util, reload, client from common import models + class GtkUi: """ The main UI class """ def __init__(self, config: dict): @@ -28,7 +29,7 @@ class GtkUi: self.ui.get_object('main_window').show_all() # Connect to data server config = config['servers'][0] - self.data_client = client.Access(config) + self.data_client = client.DataClient(self, config) # Create local data caches self.players = util.Cache() self.drinks = util.Cache() @@ -62,42 +63,6 @@ class GtkUi: 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_player(self, player: 'models.Player'): - with util.network_operation(self): - self.data_client.send_request('update_player', player) - self.full_reload() - - def update_drink(self, drink: 'models.Drink'): - with util.network_operation(self): - self.data_client.send_request('update_drink', drink) - self.full_reload() - - def save_death(self, death: 'models.Death'): - with util.network_operation(self): - self.data_client.send_request('save_death', death) - self.full_reload() - - def save_victory(self, victory: 'models.Victory'): - with util.network_operation(self): - self.data_client.send_request('save_victory', victory) - self.full_reload() - - def update_season(self, season: 'models.Season'): - with util.network_operation(self): - self.data_client.send_request('update_season', season) - self.seasons.valid = False - - def update_episode(self, episode: 'models.Episode'): - with util.network_operation(self): - self.data_client.send_request('update_episode', episode) - self.episodes.valid = False - self.season_stats.valid = False - def update_status_bar_meta(self): self.ui.get_object('connection_label').set_text(self.meta.get('connection')) self.ui.get_object('db_label').set_text(self.meta.get('database') or '') diff --git a/dsst/dsst_gtk3/handlers/base_data_handlers.py b/dsst/dsst_gtk3/handlers/base_data_handlers.py index 15738a1..083336f 100644 --- a/dsst/dsst_gtk3/handlers/base_data_handlers.py +++ b/dsst/dsst_gtk3/handlers/base_data_handlers.py @@ -9,7 +9,7 @@ class BaseDataHandlers: def do_add_player(self, entry): if entry.get_text(): - self.app.update_player(models.Player({'name': entry.get_text()})) + self.app.data_client.update_player(models.Player({'name': entry.get_text()})) entry.set_text('') def on_player_name_edited(self, _, index, value): @@ -17,29 +17,29 @@ class BaseDataHandlers: player = models.Player({'id': row[0], 'name': value, 'hex_id': row[2]}) - self.app.update_player(player) + self.app.data_client.update_player(player) def on_player_hex_edited(self, _, index, value): row = self.app.ui.get_object('all_players_store')[index] player = models.Player({'id': row[0], 'name': row[1], 'hex_id': value}) - self.app.update_player(player) + self.app.data_client.update_player(player) def do_add_drink(self, entry): if entry.get_text(): drink = models.Drink({'name': entry.get_text(), 'vol': 0.00}) - self.app.update_drink(drink) + self.app.data_client.update_drink(drink) entry.set_text('') def on_drink_name_edited(self, _, index, value): row = self.app.ui.get_object('drink_store')[index] drink = [d for d in self.app.drinks.data if d.id == row[0]][0] drink.name = value - self.app.update_drink(drink) + self.app.data_client.update_drink(drink) def on_drink_vol_edited(self, _, index, value): row = self.app.ui.get_object('drink_store')[index] 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 + self.app.data_client.update_drink(drink) \ No newline at end of file diff --git a/dsst/dsst_gtk3/handlers/death_handlers.py b/dsst/dsst_gtk3/handlers/death_handlers.py index 0522c4e..a8c5b05 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, gtk_ui +from dsst_gtk3 import dialogs, gtk_ui, util class DeathHandlers: @@ -13,7 +13,18 @@ class DeathHandlers: return death = dialogs.create_death(self.app) if death: - self.app.save_death(death) + self.app.data_client.save_death(death) def on_penalty_drink_changed(self, _, path, text): self.app.ui.get_object('player_penalties_store')[path][2] = text + + def death_tree_clicked(self, widget, event): + if event.button == 3: # right click + popup = self.app.ui.get_object('p_death') + util.show_context_menu(widget, event, popup) + return True + + def do_delete_death(self, *_): + death_id = util.get_tree_selection_value(self.app.ui.get_object('episode_deaths_tree_view'), 0) + self.app.data_client.delete_death(death_id) + diff --git a/dsst/dsst_gtk3/handlers/dialog_handlers.py b/dsst/dsst_gtk3/handlers/dialog_handlers.py index 55cb8c0..7f3ad24 100644 --- a/dsst/dsst_gtk3/handlers/dialog_handlers.py +++ b/dsst/dsst_gtk3/handlers/dialog_handlers.py @@ -36,20 +36,20 @@ class DialogHandlers: self.app.ui.get_object('enemy_optional_ckeck').set_active(False) entry.set_text('') - self.app.update_enemy(enemy) + self.app.data_client.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) + self.app.data_client.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) + self.app.data_client.update_enemy(enemy) def do_show_date_picker(self, entry: 'Gtk.Entry', *_): dialog = self.app.ui.get_object('date_picker_dialog') diff --git a/dsst/dsst_gtk3/handlers/season_handlers.py b/dsst/dsst_gtk3/handlers/season_handlers.py index c213bb1..42cf9f1 100644 --- a/dsst/dsst_gtk3/handlers/season_handlers.py +++ b/dsst/dsst_gtk3/handlers/season_handlers.py @@ -9,7 +9,7 @@ class SeasonHandlers: def do_add_season(self, *_): season = dialogs.edit_season(self.app.ui) if season: - self.app.update_season(season) + self.app.data_client.update_season(season) self.app.full_reload() def do_season_selected(self, *_): @@ -23,7 +23,7 @@ class SeasonHandlers: return ep = dialogs.edit_episode(self.app, season_id) if ep: - self.app.update_episode(ep) + self.app.data_client.update_episode(ep) self.app.full_reload() def on_selected_episode_changed(self, *_): diff --git a/dsst/dsst_gtk3/handlers/victory_handlers.py b/dsst/dsst_gtk3/handlers/victory_handlers.py index 3b47122..52c3432 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, gtk_ui +from dsst_gtk3 import dialogs, gtk_ui, util class VictoryHandlers: @@ -13,4 +13,14 @@ class VictoryHandlers: return victory = dialogs.create_victory(self.app) if victory: - self.app.save_victory(victory) + self.app.data_client.save_victory(victory) + + def victory_tree_clicked(self, widget, event): + if event.button == 3: # right click + popup = self.app.ui.get_object('p_victory') + util.show_context_menu(widget, event, popup) + return True + + def do_delete_victory(self, *_): + victory_id = util.get_tree_selection_value(self.app.ui.get_object('episode_victories_tree_view'), 0) + self.app.data_client.delete_victory(victory_id) diff --git a/dsst/dsst_gtk3/reload.py b/dsst/dsst_gtk3/reload.py index 0f232ff..80410c7 100644 --- a/dsst/dsst_gtk3/reload.py +++ b/dsst/dsst_gtk3/reload.py @@ -82,7 +82,7 @@ def reload_episode_stats(app: 'gtk_ui.GtkUi'): penalties = ['{}x {}'.format(number, drink) for drink, number in Counter(penalties).items()] penalty_string = ', '.join(penalties) 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]) + store.append([death.id, death.player.name, death.enemy.name, penalty_string, time_string, death.info]) # 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 71c2ed4..b01de10 100644 --- a/dsst/dsst_gtk3/resources/glade/window.glade +++ b/dsst/dsst_gtk3/resources/glade/window.glade @@ -1,5 +1,5 @@ - + @@ -53,6 +53,8 @@ + + @@ -101,6 +103,31 @@ 1 1 + + True + False + + + True + False + Delete + True + + + + + + True + False + + + True + False + Delete + + + + @@ -137,6 +164,9 @@ 1200 700 + + + True @@ -710,12 +740,6 @@ 7 - - - - - - False @@ -776,6 +800,7 @@ True episode_deaths_store 0 + @@ -823,6 +848,17 @@ + + + Comment + + + + 5 + + + + @@ -856,6 +892,7 @@ True True episode_victories_store + @@ -1294,9 +1331,6 @@ - - - False @@ -1306,6 +1340,9 @@ dialog False main_window + + + False @@ -1408,9 +1445,6 @@ button3 button6 - - - False @@ -1420,6 +1454,9 @@ dialog False main_window + + + False @@ -1716,9 +1753,6 @@ button1 button2 - - - False @@ -1728,6 +1762,9 @@ False main_window main_window + + + False @@ -1937,9 +1974,6 @@ button4 button5 - - - False @@ -1949,6 +1983,9 @@ dialog False main_window + + + False @@ -2220,9 +2257,6 @@ okButtonRename5 cancelButtonRename1 - - - False @@ -2233,6 +2267,9 @@ dialog False main_window + + + False @@ -2390,9 +2427,6 @@ okButtonRename3 - - - False @@ -2403,6 +2437,9 @@ dialog False main_window + + + False @@ -2573,9 +2610,6 @@ okButtonRename1 - - - False @@ -2586,6 +2620,9 @@ dialog False main_window + + + False @@ -2742,9 +2779,6 @@ okButtonRename2 - - - 100 @@ -2759,6 +2793,9 @@ dialog False main_window + + + False @@ -3120,8 +3157,5 @@ okButtonRename4 cancelButtonRename4 - - - diff --git a/dsst/dsst_gtk3/util.py b/dsst/dsst_gtk3/util.py index 2a71c0c..057a7ea 100644 --- a/dsst/dsst_gtk3/util.py +++ b/dsst/dsst_gtk3/util.py @@ -75,6 +75,17 @@ def get_combo_value(combo, index: int): return -1 +def get_tree_selection_value(tree: 'Gtk.TreeView', column: int): + """ Retrieve the a cell value of a tree view based on selected row and the choosen column + :param tree: Gtk.TreeView widget + :param column: Number of the column from which to retrieve the value + """ + (model, pathlist) = tree.get_selection().get_selected_rows() + for path in pathlist: + tree_iter = model.get_iter(path) + return model.get_value(tree_iter, 0) + + def get_index_of_combo_model(widget, column: int, value: int): """Get the index of a value within a Gtk widgets model based on column an value :param widget: Any Gtk widget that can be bound to a ListStore or TreeStore @@ -147,3 +158,17 @@ def save_config(config: dict, config_path: str): os.mkdir(path) with open(config_path, 'wb') as file: file.write(json.dumps(config, sort_keys=True, indent=4, separators=(',', ': ')).encode('utf-8')) + + +def show_context_menu(tree: 'Gtk.TreeView', event, popup: 'Gtk.Menu'): + path = tree.get_path_at_pos(int(event.x), int(event.y)) + # Get the selection + selection = tree.get_selection() + # Get the selected path(s) + rows = selection.get_selected_rows() + # If not clicked on selection, change selected rows + if path: + if path[0] not in rows[1]: + selection.unselect_all() + selection.select_path(path[0]) + popup.popup(None, None, None, None, 0, event.time) diff --git a/dsst/dsst_server/data_access/sql.py b/dsst/dsst_server/data_access/sql.py index 90949d9..c7a49ec 100644 --- a/dsst/dsst_server/data_access/sql.py +++ b/dsst/dsst_server/data_access/sql.py @@ -12,7 +12,7 @@ try: from peewee import * except ImportError: print('peewee package not installed') - sys.exit(0) + sys.exit(1) db = MySQLDatabase(None) @@ -43,7 +43,7 @@ class Episode(Model): number = CharField() name = CharField(null=True) date = DateField(null=True) - season = ForeignKeyField(Season, backref='episodes') + season = ForeignKeyField(Season, backref='episodes', on_delete='CASCADE') players = ManyToManyField(Player, backref='episodes') class Meta: @@ -63,7 +63,7 @@ class Enemy(Model): id = AutoField() name = CharField() boss = BooleanField() - season = ForeignKeyField(Season, backref='enemies') + season = ForeignKeyField(Season, backref='enemies', on_delete='CASCADE') class Meta: database = db @@ -75,7 +75,7 @@ class Death(Model): time = TimeField(default=datetime.time(0, 0)) player = ForeignKeyField(Player) enemy = ForeignKeyField(Enemy) - episode = ForeignKeyField(Episode, backref='deaths') + episode = ForeignKeyField(Episode, backref='deaths', on_delete='CASCADE') class Meta: database = db @@ -85,8 +85,8 @@ class Penalty(Model): id = AutoField() size = DecimalField() drink = ForeignKeyField(Drink) - player = ForeignKeyField(Player, backref='penalties') - death = ForeignKeyField(Death, backref='penalties') + player = ForeignKeyField(Player, backref='penalties', on_delete='CASCADE') + death = ForeignKeyField(Death, backref='penalties', on_delete='CASCADE') class Meta: database = db @@ -98,7 +98,7 @@ class Victory(Model): time = TimeField(default=datetime.time(0, 0)) player = ForeignKeyField(Player) enemy = ForeignKeyField(Enemy) - episode = ForeignKeyField(Episode, backref='victories') + episode = ForeignKeyField(Episode, backref='victories', on_delete='CASCADE') class Meta: database = db diff --git a/dsst/dsst_server/func_delete.py b/dsst/dsst_server/func_delete.py new file mode 100644 index 0000000..46ba68a --- /dev/null +++ b/dsst/dsst_server/func_delete.py @@ -0,0 +1,15 @@ +from dsst_server.data_access import sql +from dsst_server.auth import check_write + + +class DeleteFunctions: + + @staticmethod + @check_write + def delete_death(death_id: int): + return sql.Death.delete().where(sql.Death.id == death_id).execute() + + @staticmethod + @check_write + def delete_victory(victory_id: int): + return sql.Victory.delete().where(sql.Death.id == victory_id).execute() diff --git a/dsst/dsst_server/func_proxy.py b/dsst/dsst_server/func_proxy.py index 326f1a2..0749b2f 100644 --- a/dsst/dsst_server/func_proxy.py +++ b/dsst/dsst_server/func_proxy.py @@ -1,6 +1,7 @@ from dsst_server.func_write import WriteFunctions from dsst_server.func_read import ReadFunctions +from dsst_server.func_delete import DeleteFunctions -class FunctionProxy(WriteFunctions, ReadFunctions): +class FunctionProxy(WriteFunctions, ReadFunctions, DeleteFunctions): pass