Compare commits

6 Commits

Author SHA1 Message Date
luxick
cb8646bae2 Delete functions on client. 2018-05-05 15:52:12 +02:00
luxick
b3eca219ac Create database connection for each request
To mitigate "Error 2006: MySQL server has gone away"
2018-04-11 12:28:48 +02:00
luxick
def5ff5ea6 Improve logging on server
Log to file and std. Configured in server config file.
2018-03-28 22:54:02 +02:00
luxick
ad85a37d5e Use function decorators to check authentication tokens. 2018-03-28 20:58:19 +02:00
luxick
4b8d2421a5 Move auth tokens to server config. 2018-03-15 13:14:54 +01:00
luxick
b5df3289b4 Merge pull request #1 from luxick/client_server
Merge Client/Server Branch
2018-03-14 19:45:47 +01:00
22 changed files with 565 additions and 220 deletions

2
.gitignore vendored
View File

@@ -91,3 +91,5 @@ ENV/
.idea
install.txt
Screenshots/
.vscode

View File

@@ -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]

View File

@@ -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')
@@ -31,10 +47,145 @@ class Access:
soc.close()
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)

View File

@@ -102,6 +102,13 @@ def create_death(app: 'gtk_ui.GtkUi'):
:param app: Main Gtk application
:return: Death object or None if dialog was canceled
"""
# Set penalties
default_drink = app.drinks.data[0].name
store = app.ui.get_object('player_penalties_store')
store.clear()
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()

View File

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

View File

@@ -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)
self.app.data_client.update_drink(drink)

View File

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

View File

@@ -29,7 +29,6 @@ class DialogHandlers:
def do_add_enemy(self, entry):
if entry.get_text():
store = self.app.ui.get_object('enemy_season_store')
enemy = models.Enemy()
enemy.name = entry.get_text()
enemy.season = self.app.get_selected_season_id()
@@ -37,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')

View File

@@ -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, *_):

View File

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

View File

@@ -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()
@@ -96,12 +96,22 @@ def reload_episode_stats(app: 'gtk_ui.GtkUi'):
drink_count = sum(len(death.penalties) for death in 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)))
# Compute booze stats
dl_booze = sum(len(death.penalties) * death.penalties[0].size for death in episode.deaths)
l_booze = round(dl_booze / 10, 2)
player_ml_booze = round((dl_booze * 100) / len(episode.players), 2)
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)
app.ui.get_object('ep_player_booze_label').set_text('{}ml'.format(ml_booze))
app.ui.get_object('ep_player_booze_label').set_text('{}ml'.format(player_ml_booze))
# Compute pure alc stats
deaths = episode.deaths
dl_alc = 0
for death in deaths:
dl_alc += sum((penalty.size / 100) * penalty.drink.vol for penalty in death.penalties)
ml_alc = round(dl_alc * 100, 2)
app.ui.get_object('ep_alc_label').set_text('{}ml'.format(ml_alc))
# Compute hardest enemy stats
enemy_list = [death.enemy.name for death in episode.deaths]
sorted_list = Counter(enemy_list).most_common(1)
if sorted_list:

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.4 -->
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.16"/>
<object class="GtkListStore" id="all_players_store">
@@ -53,6 +53,8 @@
<column type="gchararray"/>
<!-- column-name time -->
<column type="gchararray"/>
<!-- column-name info -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkListStore" id="episode_players_store">
@@ -101,6 +103,31 @@
<property name="step_increment">1</property>
<property name="page_increment">1</property>
</object>
<object class="GtkMenu" id="p_death">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkMenuItem" id="del_death">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Delete</property>
<property name="use_underline">True</property>
<signal name="activate" handler="do_delete_death" swapped="no"/>
</object>
</child>
</object>
<object class="GtkMenu" id="p_victory">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkMenuItem" id="del_victory">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Delete</property>
<signal name="activate" handler="do_delete_victory" swapped="no"/>
</object>
</child>
</object>
<object class="GtkListStore" id="player_penalties_store">
<columns>
<!-- column-name penalty_id -->
@@ -137,6 +164,9 @@
<property name="default_width">1200</property>
<property name="default_height">700</property>
<signal name="delete-event" handler="do_delete_event" swapped="no"/>
<child type="titlebar">
<placeholder/>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
@@ -167,7 +197,7 @@
</object>
</child>
<child>
<object class="GtkMenuItem">
<object class="GtkMenuItem" id="add_episode_item">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Add Episode</property>
@@ -220,7 +250,7 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkMenuItem">
<object class="GtkMenuItem" id="add_death_item">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Add Death</property>
@@ -230,7 +260,7 @@
</object>
</child>
<child>
<object class="GtkMenuItem">
<object class="GtkMenuItem" id="add_victory_item">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Add Victory</property>
@@ -263,7 +293,7 @@
</object>
</child>
<child>
<object class="GtkMenuItem">
<object class="GtkMenuItem" id="manage_drinks_item">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Manage Drinks</property>
@@ -542,7 +572,7 @@
<object class="GtkLabel" id="ep_death_count_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">[DeathCount]</property>
<property name="label" translatable="yes">Computing...</property>
</object>
<packing>
<property name="left_attach">1</property>
@@ -553,51 +583,7 @@
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Most Deaths:</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">7</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Hardest Enemy:</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">8</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ep_enemy_name_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">7</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ep_worst_player_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">8</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Total Drinks:</property>
<property name="label" translatable="yes">Total Shots:</property>
<property name="xalign">1</property>
</object>
<packing>
@@ -609,7 +595,7 @@
<object class="GtkLabel" id="ep_drinks_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">[DrinkCount]</property>
<property name="label" translatable="yes">Computing...</property>
</object>
<packing>
<property name="left_attach">1</property>
@@ -632,7 +618,7 @@
<object class="GtkLabel" id="ep_booze_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">[ToatalBooze]</property>
<property name="label" translatable="yes">Computing...</property>
</object>
<packing>
<property name="left_attach">1</property>
@@ -691,7 +677,7 @@
<object class="GtkLabel" id="ep_player_drinks_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">[PlayerDrinks]</property>
<property name="label" translatable="yes">Computing...</property>
</object>
<packing>
<property name="left_attach">1</property>
@@ -702,7 +688,7 @@
<object class="GtkLabel" id="ep_player_booze_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">[PlayerBooze]</property>
<property name="label" translatable="yes">Computing...</property>
</object>
<packing>
<property name="left_attach">1</property>
@@ -713,7 +699,7 @@
<object class="GtkLabel" id="ep_player_alc_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">[PlayerAlc]</property>
<property name="label" translatable="yes">Computing...</property>
</object>
<packing>
<property name="left_attach">1</property>
@@ -724,13 +710,36 @@
<object class="GtkLabel" id="ep_alc_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">[TotalAlc]</property>
<property name="label" translatable="yes">Computing...</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">6</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Hardest Enemy:</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">7</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ep_enemy_name_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Computing...</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">7</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -791,6 +800,7 @@
<property name="can_focus">True</property>
<property name="model">episode_deaths_store</property>
<property name="search_column">0</property>
<signal name="button-press-event" handler="death_tree_clicked" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
@@ -838,6 +848,17 @@
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn">
<property name="title" translatable="yes">Comment</property>
<child>
<object class="GtkCellRendererText"/>
<attributes>
<attribute name="text">5</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
@@ -871,6 +892,7 @@
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">episode_victories_store</property>
<signal name="button-press-event" handler="victory_tree_clicked" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
@@ -1309,9 +1331,6 @@
</child>
</object>
</child>
<child type="titlebar">
<placeholder/>
</child>
</object>
<object class="GtkDialog" id="date_picker_dialog">
<property name="can_focus">False</property>
@@ -1321,6 +1340,9 @@
<property name="type_hint">dialog</property>
<property name="deletable">False</property>
<property name="transient_for">main_window</property>
<child>
<placeholder/>
</child>
<child internal-child="vbox">
<object class="GtkBox">
<property name="can_focus">False</property>
@@ -1423,9 +1445,6 @@
<action-widget response="-5">button3</action-widget>
<action-widget response="-6">button6</action-widget>
</action-widgets>
<child>
<placeholder/>
</child>
</object>
<object class="GtkDialog" id="edit_episode_dialog">
<property name="can_focus">False</property>
@@ -1435,6 +1454,9 @@
<property name="type_hint">dialog</property>
<property name="deletable">False</property>
<property name="transient_for">main_window</property>
<child>
<placeholder/>
</child>
<child internal-child="vbox">
<object class="GtkBox">
<property name="can_focus">False</property>
@@ -1731,9 +1753,6 @@
<action-widget response="-5">button1</action-widget>
<action-widget response="-6">button2</action-widget>
</action-widgets>
<child>
<placeholder/>
</child>
</object>
<object class="GtkDialog" id="edit_season_dialog">
<property name="can_focus">False</property>
@@ -1743,6 +1762,9 @@
<property name="deletable">False</property>
<property name="transient_for">main_window</property>
<property name="attached_to">main_window</property>
<child>
<placeholder/>
</child>
<child internal-child="vbox">
<object class="GtkBox">
<property name="can_focus">False</property>
@@ -1952,9 +1974,6 @@
<action-widget response="-5">button4</action-widget>
<action-widget response="-6">button5</action-widget>
</action-widgets>
<child>
<placeholder/>
</child>
</object>
<object class="GtkDialog" id="edit_victory_dialog">
<property name="can_focus">False</property>
@@ -1964,6 +1983,9 @@
<property name="type_hint">dialog</property>
<property name="deletable">False</property>
<property name="transient_for">main_window</property>
<child>
<placeholder/>
</child>
<child internal-child="vbox">
<object class="GtkBox">
<property name="can_focus">False</property>
@@ -2235,9 +2257,6 @@
<action-widget response="-5">okButtonRename5</action-widget>
<action-widget response="-6">cancelButtonRename1</action-widget>
</action-widgets>
<child>
<placeholder/>
</child>
</object>
<object class="GtkDialog" id="manage_drinks_dialog">
<property name="can_focus">False</property>
@@ -2248,6 +2267,9 @@
<property name="type_hint">dialog</property>
<property name="deletable">False</property>
<property name="transient_for">main_window</property>
<child>
<placeholder/>
</child>
<child internal-child="vbox">
<object class="GtkBox">
<property name="can_focus">False</property>
@@ -2405,9 +2427,6 @@
<action-widgets>
<action-widget response="-5">okButtonRename3</action-widget>
</action-widgets>
<child>
<placeholder/>
</child>
</object>
<object class="GtkDialog" id="manage_enemies_dialog">
<property name="can_focus">False</property>
@@ -2418,6 +2437,9 @@
<property name="type_hint">dialog</property>
<property name="deletable">False</property>
<property name="transient_for">main_window</property>
<child>
<placeholder/>
</child>
<child internal-child="vbox">
<object class="GtkBox">
<property name="can_focus">False</property>
@@ -2588,9 +2610,6 @@
<action-widgets>
<action-widget response="-5">okButtonRename1</action-widget>
</action-widgets>
<child>
<placeholder/>
</child>
</object>
<object class="GtkDialog" id="manage_players_dialog">
<property name="can_focus">False</property>
@@ -2601,6 +2620,9 @@
<property name="type_hint">dialog</property>
<property name="deletable">False</property>
<property name="transient_for">main_window</property>
<child>
<placeholder/>
</child>
<child internal-child="vbox">
<object class="GtkBox">
<property name="can_focus">False</property>
@@ -2757,9 +2779,6 @@
<action-widgets>
<action-widget response="-5">okButtonRename2</action-widget>
</action-widgets>
<child>
<placeholder/>
</child>
</object>
<object class="GtkAdjustment" id="shot_size_adjustment">
<property name="upper">100</property>
@@ -2774,6 +2793,9 @@
<property name="type_hint">dialog</property>
<property name="deletable">False</property>
<property name="transient_for">main_window</property>
<child>
<placeholder/>
</child>
<child internal-child="vbox">
<object class="GtkBox">
<property name="can_focus">False</property>
@@ -3135,8 +3157,5 @@
<action-widget response="-5">okButtonRename4</action-widget>
<action-widget response="-6">cancelButtonRename4</action-widget>
</action-widgets>
<child>
<placeholder/>
</child>
</object>
</interface>

View File

@@ -14,7 +14,7 @@ DEFAULT_CONFIG = {
'auto_connect': False,
'servers': [{
'host': 'localhost',
'port': 12345,
'port': 55225,
'buffer_size': 1024,
'auth_token': ''}
]
@@ -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)

33
dsst/dsst_server/auth.py Normal file
View File

@@ -0,0 +1,33 @@
READ_TOKENS = []
WRITE_TOKENS = []
class AuthenticationError(Exception):
def __init__(self, message):
self.message = message
def get_response(self):
return {
'success': False,
'message': 'Authentication Failed:\n'.format(self.message)
}
def check_read(func):
def wrapper(*args, **kwargs):
token = args[0]
if token in READ_TOKENS + WRITE_TOKENS:
return func(*args[1:], **kwargs)
else:
raise AuthenticationError('Token "{}" has no read access on database.'.format(token))
return wrapper
def check_write(func):
def wrapper(*args, **kwargs):
token = args[0]
if token in WRITE_TOKENS:
return func(*args[1:], **kwargs)
else:
raise AuthenticationError('Token "{}" has no write access on database.'.format(token))
return wrapper

View File

@@ -0,0 +1,45 @@
# This config will be copied on first launch.
# Edit the the copied file at '~/.config/dsst/server.json' before launching the server
# IMPORTANT: For a client to access the server you have to set authentication tokens in the server config
DEFAULT_CONFIG = {
'database': {
'db_name': 'dsst',
'user': 'dsst',
'password': 'dsst'
},
'server': {
'port': 55225,
'buffer_size': 1024
},
'tokens': {
'readonly': [],
'readwrite': []
},
'logging': dict(
version=1,
disable_existing_loggers=False,
formatters={
'file': {'format': '[%(levelname)s] %(asctime)s - %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S'},
'std': {'format': '[%(levelname)s] %(message)s'}
},
handlers={
'console': {
'class': 'logging.StreamHandler',
'formatter': 'std',
'level': 'INFO'
},
'logfile': {
'class': 'logging.handlers.RotatingFileHandler',
'maxBytes': 3145728,
'backupCount': 1,
'formatter': 'file',
'level': 'INFO'
}
},
root={
'handlers': ['console', 'logfile'],
'level': 'INFO'
})
}

View File

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

View File

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

View File

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

View File

@@ -1,40 +1,49 @@
from dsst_server.data_access import sql, sql_func, mapping
from dsst_server.auth import check_read
from common import models
from playhouse import shortcuts
class ReadFunctions:
@staticmethod
@check_read
def load_db_meta(*_):
return sql.db.database
@staticmethod
@check_read
def load_seasons(*_):
return [mapping.db_to_season(season) for season in sql.Season.select()]
@staticmethod
@check_read
def load_seasons_all(*_):
return [shortcuts.model_to_dict(season, backrefs=True, max_depth=2) for season in sql.Season.select()]
@staticmethod
@check_read
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
@check_read
def load_players(*_):
return [mapping.db_to_player(player) for player in sql.Player.select()]
@staticmethod
@check_read
def load_enemies(season_id, *_):
pass
@staticmethod
@check_read
def load_drinks(*_):
return [mapping.db_to_drink(drink) for drink in sql.Drink.select()]
@staticmethod
@check_read
def load_season_stats(season_id, *_):
season = sql.Season.get(sql.Season.id == season_id)
players = sql_func.players_for_season(season_id)

View File

@@ -1,13 +1,16 @@
from common import models
from dsst_server.data_access import sql
from dsst_server.auth import check_write
class WriteFunctions:
@staticmethod
@check_write
def create_season(season: 'models.Season'):
return 'Season created.'
@staticmethod
@check_write
def update_enemy(enemy: 'models.Enemy', *_):
(sql.Enemy
.insert(id=enemy.id, boss=enemy.boss, name=enemy.name, season=enemy.season)
@@ -17,6 +20,7 @@ class WriteFunctions:
.execute())
@staticmethod
@check_write
def update_player(player: 'models.Player', *_):
(sql.Player
.insert(id=player.id, name=player.name, hex_id=player.hex_id)
@@ -25,6 +29,7 @@ class WriteFunctions:
.execute())
@staticmethod
@check_write
def update_drink(drink: 'models.Drink', *_):
(sql.Drink
.insert(id=drink.id, name=drink.name, vol=drink.vol)
@@ -33,6 +38,7 @@ class WriteFunctions:
.execute())
@staticmethod
@check_write
def save_death(death: 'models.Death'):
with sql.db.atomic():
created_id = (sql.Death
@@ -43,6 +49,7 @@ class WriteFunctions:
sql.Penalty.create(death=created_id, size=penalty.size, drink=penalty.drink, player=penalty.player)
@staticmethod
@check_write
def save_victory(victory: 'models.Victory'):
(sql.Victory
.insert(info=victory.info, player=victory.player, enemy=victory.enemy, time=victory.time,
@@ -50,6 +57,7 @@ class WriteFunctions:
.execute())
@staticmethod
@check_write
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,
@@ -62,6 +70,7 @@ class WriteFunctions:
.execute())
@staticmethod
@check_write
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
@@ -76,3 +85,8 @@ class WriteFunctions:
db_episode = sql.Episode.get(sql.Episode.id == new_ep_id)
db_episode.players = players
db_episode.save()
@staticmethod
@check_write
def delete_player(player_id: int, *_):
sql.Player.delete_by_id(int)

View File

@@ -1,83 +1,102 @@
import json
import pickle
import socket
import sys
import os
import logging
from logging.config import dictConfig
from common import util, models
from dsst_server import func_read, func_write, tokens
from common import util
from dsst_server import auth
from dsst_server.func_proxy import FunctionProxy
from dsst_server.data_access import sql, sql_func
PORT = 12345
HOST = socket.gethostname()
BUFFER_SIZE = 1024
from dsst_server.config import DEFAULT_CONFIG
class DsstServer:
def __init__(self, config={}):
def __init__(self, config):
# Initialize the logger
try:
logger_dict = config.get('logging')
logfile = os.path.join(os.path.expanduser("~"), '.config', 'dsst', 'server.log')
logger_dict.get('handlers').get('logfile')['filename'] = logfile
dictConfig(logger_dict)
except Exception as e:
print(e)
sys.exit(1)
# Create ands bind the socket server
self.socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print('Created socket')
logging.info('Created socket')
server_conf = config.get('server')
try:
self.socket_server.bind(('', server_conf.get('port')))
logging.info('Bound socket to port {}'.format(server_conf.get('port')))
except OSError as e:
logging.error('Cannot bind socket: {})'.format(e.strerror))
sys.exit(1)
self.socket_server.bind((HOST, PORT))
print('Bound socket to {} on host {}'.format(PORT, HOST))
# Initialize database
# Save Database credentials
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))
self.db_user = db_config.get('user')
self.db_name = db_config.get('db_name')
self.db_password = db_config.get('password')
# Load access tokens and map them to their allowed methods
read_actions = util.list_class_methods(func_read.ReadFunctions)
write_actions = util.list_class_methods(func_write.WriteFunctions)
parm_access = {
'r': read_actions,
'rw': read_actions + write_actions
}
self.tokens = {token: parm_access[perms] for token, perms in tokens.TOKENS}
print('Loaded auth tokens: {}'.format(self.tokens.keys()))
# Load access tokens
auth.READ_TOKENS = config.get('tokens').get('readonly')
auth.WRITE_TOKENS = config.get('tokens').get('readwrite')
logging.info('Auth tokens loaded')
def process_request(self, request: dict) -> dict:
""" Process a requested function from a client
:param request: Request dictionary
:return: Response dictionary
"""
# Get requested function from the function proxy
action_name = request.get('action')
action = getattr(FunctionProxy, action_name)
try:
# Open a database connection
sql.db.init(self.db_name, user=self.db_user, password=self.db_password)
logging.info('Connected to database ({})'.format(self.db_name))
# Execute the function
result = action(request.get('auth_token'), *request.get('args'))
logging.info('Operation executed successfully')
return {'success': True, 'data': result}
except auth.AuthenticationError as e:
logging.error(e.get_response())
return e.get_response()
except Exception as e:
logging.error('Exception was thrown: ' + str(e))
return {'success': False, 'message': 'Exception was thrown on server.'}
finally:
sql.db.close()
logging.info('Database connection closed')
def run(self):
self.socket_server.listen(5)
print('Socket is listening')
logging.info('Socket is listening')
while True:
# Accept client connection
client, address = self.socket_server.accept()
try:
print('Connection from {}'.format(address))
data = util.recv_msg(client)
request = pickle.loads(data)
print('Request: {}'.format(request))
# Validate auth token in request
token = request.get('auth_token')
if token not in self.tokens:
util.send_msg(client, pickle.dumps({'success': False, 'message': 'Auth token invalid'}))
print('Rejected request from {}. Auth token invalid ({})'.format(address, token))
continue
# Check read functions
action_name = request.get('action')
if action_name in self.tokens[token]:
action = getattr(FunctionProxy, action_name)
try:
value = action(*request.get('args'))
except Exception as e:
response = {'success': False, 'message': 'Exception was thrown on server.\n{}'.format(e)}
util.send_msg(client, pickle.dumps(response))
raise
response = {'success': True, 'data': value}
util.send_msg(client, pickle.dumps(response))
continue
else:
msg = 'Action does not exist on server ({})'.format(request.get('action'))
util.send_msg(client, pickle.dumps({'success': False, 'message': msg}))
except Exception as e:
print(e)
finally:
client.close()
print('Connection to client closed')
logging.info('-' * 30)
logging.info('Connection from {}'.format(address))
# Parse request from client
data = util.recv_msg(client)
request = pickle.loads(data)
logging.info('Request: {}'.format(request))
# Process request
response = self.process_request(request)
# Send data back to client
util.send_msg(client, pickle.dumps(response))
# Close connection
client.close()
logging.info('Connection to client closed')
def load_config(config_path: str) -> dict:
@@ -85,13 +104,28 @@ def load_config(config_path: str) -> dict:
return json.load(config_file)
def save_config(config: dict, config_path: str):
path = os.path.dirname(config_path)
if not os.path.isdir(path):
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 main():
config = os.path.join(os.path.expanduser('~'), '.config', 'dsst', 'server.json')
if not os.path.isfile(config):
save_config(DEFAULT_CONFIG, config)
logging.error('No server config file found.\n'
'Copied default config to "{}"\n'
'Please edit file before starting server.'
.format(config))
sys.exit(0)
server = DsstServer(load_config(config))
try:
server.run()
except KeyboardInterrupt:
print('Server stopped')
logging.info('Server stopped')
server.socket_server.close()
try:
sys.exit(0)

View File

@@ -1,5 +0,0 @@
# Define access tokens here
# i.E: { 'read': ['a', 'b''],
# 'write': ['a']
# }
TOKENS = [('a', 'rw'), ('b', 'r')]