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 .idea
install.txt install.txt
Screenshots/
.vscode

View File

@@ -9,8 +9,8 @@ import shutil
INTERPRETER = '/usr/bin/env python3' INTERPRETER = '/usr/bin/env python3'
CLIENT_VERSION = '0.1' CLIENT_VERSION = '0.2'
SERVER_VERSION = '0.1' SERVER_VERSION = '0.2'
try: try:
build_mode = sys.argv[1] build_mode = sys.argv[1]

View File

@@ -5,10 +5,26 @@ try:
import cPickle as pickle import cPickle as pickle
except ImportError: except ImportError:
import pickle import pickle
from dsst_gtk3 import gtk_ui
class Access: def gui_handled(func):
def __init__(self, conn_dict): 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.host = conn_dict.get('host')
self.port = conn_dict.get('port') self.port = conn_dict.get('port')
self.buffer = conn_dict.get('buffer_size') self.buffer = conn_dict.get('buffer_size')
@@ -31,10 +47,145 @@ class Access:
soc.close() soc.close()
return message.get('data') 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__': 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' action = 'load_seasons'
response = access.send_request(action) response = access.send_request(action)
pp = pprint.PrettyPrinter(indent=1) pp = pprint.PrettyPrinter(indent=1)
for s in response: for s in response:
pp.pprint(s.__dict__) 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 :param app: Main Gtk application
:return: Death object or None if dialog was canceled :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 # Run the dialog
dialog = app.ui.get_object("edit_death_dialog") # type: Gtk.Dialog dialog = app.ui.get_object("edit_death_dialog") # type: Gtk.Dialog
result = dialog.run() result = dialog.run()

View File

@@ -6,6 +6,7 @@ from dsst_gtk3.handlers import handlers
from dsst_gtk3 import util, reload, client from dsst_gtk3 import util, reload, client
from common import models from common import models
class GtkUi: class GtkUi:
""" The main UI class """ """ The main UI class """
def __init__(self, config: dict): def __init__(self, config: dict):
@@ -28,7 +29,7 @@ class GtkUi:
self.ui.get_object('main_window').show_all() self.ui.get_object('main_window').show_all()
# Connect to data server # Connect to data server
config = config['servers'][0] config = config['servers'][0]
self.data_client = client.Access(config) self.data_client = client.DataClient(self, config)
# Create local data caches # Create local data caches
self.players = util.Cache() self.players = util.Cache()
self.drinks = util.Cache() self.drinks = util.Cache()
@@ -62,42 +63,6 @@ class GtkUi:
def reload(self): def reload(self):
pass 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): def update_status_bar_meta(self):
self.ui.get_object('connection_label').set_text(self.meta.get('connection')) 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 '') 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): def do_add_player(self, entry):
if entry.get_text(): 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('') entry.set_text('')
def on_player_name_edited(self, _, index, value): def on_player_name_edited(self, _, index, value):
@@ -17,29 +17,29 @@ class BaseDataHandlers:
player = models.Player({'id': row[0], player = models.Player({'id': row[0],
'name': value, 'name': value,
'hex_id': row[2]}) 'hex_id': row[2]})
self.app.update_player(player) self.app.data_client.update_player(player)
def on_player_hex_edited(self, _, index, value): def on_player_hex_edited(self, _, index, value):
row = self.app.ui.get_object('all_players_store')[index] row = self.app.ui.get_object('all_players_store')[index]
player = models.Player({'id': row[0], player = models.Player({'id': row[0],
'name': row[1], 'name': row[1],
'hex_id': value}) 'hex_id': value})
self.app.update_player(player) self.app.data_client.update_player(player)
def do_add_drink(self, entry): def do_add_drink(self, entry):
if entry.get_text(): if entry.get_text():
drink = models.Drink({'name': entry.get_text(), 'vol': 0.00}) 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('') entry.set_text('')
def on_drink_name_edited(self, _, index, value): def on_drink_name_edited(self, _, index, value):
row = self.app.ui.get_object('drink_store')[index] 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 = [d for d in self.app.drinks.data if d.id == row[0]][0]
drink.name = value drink.name = value
self.app.update_drink(drink) self.app.data_client.update_drink(drink)
def on_drink_vol_edited(self, _, index, value): def on_drink_vol_edited(self, _, index, value):
row = self.app.ui.get_object('drink_store')[index] 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 = [d for d in self.app.drinks.data if d.id == row[0]][0]
drink.vol = value 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 gi.repository import Gtk
from dsst_gtk3 import dialogs, gtk_ui from dsst_gtk3 import dialogs, gtk_ui, util
class DeathHandlers: class DeathHandlers:
@@ -13,7 +13,18 @@ class DeathHandlers:
return return
death = dialogs.create_death(self.app) death = dialogs.create_death(self.app)
if death: if death:
self.app.save_death(death) self.app.data_client.save_death(death)
def on_penalty_drink_changed(self, _, path, text): def on_penalty_drink_changed(self, _, path, text):
self.app.ui.get_object('player_penalties_store')[path][2] = 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): def do_add_enemy(self, entry):
if entry.get_text(): if entry.get_text():
store = self.app.ui.get_object('enemy_season_store')
enemy = models.Enemy() enemy = models.Enemy()
enemy.name = entry.get_text() enemy.name = entry.get_text()
enemy.season = self.app.get_selected_season_id() 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) self.app.ui.get_object('enemy_optional_ckeck').set_active(False)
entry.set_text('') entry.set_text('')
self.app.update_enemy(enemy) self.app.data_client.update_enemy(enemy)
def on_enemy_name_edited(self, _, index, value): def on_enemy_name_edited(self, _, index, value):
row = self.app.ui.get_object('enemy_season_store')[index] 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 = [enemy for enemy in self.app.enemies.data if enemy.id == row[4]][0]
enemy.name = value enemy.name = value
self.app.update_enemy(enemy) self.app.data_client.update_enemy(enemy)
def on_enemy_optional_edited(self, renderer, index): def on_enemy_optional_edited(self, renderer, index):
new_optional_value = not renderer.get_active() new_optional_value = not renderer.get_active()
row = self.app.ui.get_object('enemy_season_store')[index] 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 = [enemy for enemy in self.app.enemies.data if enemy.id == row[4]][0]
enemy.boss = new_optional_value 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', *_): def do_show_date_picker(self, entry: 'Gtk.Entry', *_):
dialog = self.app.ui.get_object('date_picker_dialog') dialog = self.app.ui.get_object('date_picker_dialog')

View File

@@ -9,7 +9,7 @@ class SeasonHandlers:
def do_add_season(self, *_): def do_add_season(self, *_):
season = dialogs.edit_season(self.app.ui) season = dialogs.edit_season(self.app.ui)
if season: if season:
self.app.update_season(season) self.app.data_client.update_season(season)
self.app.full_reload() self.app.full_reload()
def do_season_selected(self, *_): def do_season_selected(self, *_):
@@ -23,7 +23,7 @@ class SeasonHandlers:
return return
ep = dialogs.edit_episode(self.app, season_id) ep = dialogs.edit_episode(self.app, season_id)
if ep: if ep:
self.app.update_episode(ep) self.app.data_client.update_episode(ep)
self.app.full_reload() self.app.full_reload()
def on_selected_episode_changed(self, *_): def on_selected_episode_changed(self, *_):

View File

@@ -1,5 +1,5 @@
from gi.repository import Gtk from gi.repository import Gtk
from dsst_gtk3 import dialogs, gtk_ui from dsst_gtk3 import dialogs, gtk_ui, util
class VictoryHandlers: class VictoryHandlers:
@@ -13,4 +13,14 @@ class VictoryHandlers:
return return
victory = dialogs.create_victory(self.app) victory = dialogs.create_victory(self.app)
if victory: 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()] penalties = ['{}x {}'.format(number, drink) for drink, number in Counter(penalties).items()]
penalty_string = ', '.join(penalties) penalty_string = ', '.join(penalties)
time_string = '{:02d}:{:02d}'.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]) store.append([death.id, death.player.name, death.enemy.name, penalty_string, time_string, death.info])
# Reload victory store for notebook view # Reload victory store for notebook view
store = app.ui.get_object('episode_victories_store') store = app.ui.get_object('episode_victories_store')
store.clear() 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) 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_drinks_label').set_text(str(drink_count))
app.ui.get_object('ep_player_drinks_label').set_text(str(len(episode.deaths))) 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) dl_booze = sum(len(death.penalties) * death.penalties[0].size for death in episode.deaths)
l_booze = round(dl_booze / 10, 2) 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)) 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) app.ui.get_object('ep_player_booze_label').set_text('{}ml'.format(player_ml_booze))
ml_booze = round(dl_booze * 10, 0)
app.ui.get_object('ep_player_booze_label').set_text('{}ml'.format(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] enemy_list = [death.enemy.name for death in episode.deaths]
sorted_list = Counter(enemy_list).most_common(1) sorted_list = Counter(enemy_list).most_common(1)
if sorted_list: if sorted_list:

View File

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

View File

@@ -14,7 +14,7 @@ DEFAULT_CONFIG = {
'auto_connect': False, 'auto_connect': False,
'servers': [{ 'servers': [{
'host': 'localhost', 'host': 'localhost',
'port': 12345, 'port': 55225,
'buffer_size': 1024, 'buffer_size': 1024,
'auth_token': ''} 'auth_token': ''}
] ]
@@ -75,6 +75,17 @@ def get_combo_value(combo, index: int):
return -1 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): 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 """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 :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) os.mkdir(path)
with open(config_path, 'wb') as file: with open(config_path, 'wb') as file:
file.write(json.dumps(config, sort_keys=True, indent=4, separators=(',', ': ')).encode('utf-8')) 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 * from peewee import *
except ImportError: except ImportError:
print('peewee package not installed') print('peewee package not installed')
sys.exit(0) sys.exit(1)
db = MySQLDatabase(None) db = MySQLDatabase(None)
@@ -43,7 +43,7 @@ class Episode(Model):
number = CharField() number = CharField()
name = CharField(null=True) name = CharField(null=True)
date = DateField(null=True) date = DateField(null=True)
season = ForeignKeyField(Season, backref='episodes') season = ForeignKeyField(Season, backref='episodes', on_delete='CASCADE')
players = ManyToManyField(Player, backref='episodes') players = ManyToManyField(Player, backref='episodes')
class Meta: class Meta:
@@ -63,7 +63,7 @@ class Enemy(Model):
id = AutoField() id = AutoField()
name = CharField() name = CharField()
boss = BooleanField() boss = BooleanField()
season = ForeignKeyField(Season, backref='enemies') season = ForeignKeyField(Season, backref='enemies', on_delete='CASCADE')
class Meta: class Meta:
database = db database = db
@@ -75,7 +75,7 @@ class Death(Model):
time = TimeField(default=datetime.time(0, 0)) time = TimeField(default=datetime.time(0, 0))
player = ForeignKeyField(Player) player = ForeignKeyField(Player)
enemy = ForeignKeyField(Enemy) enemy = ForeignKeyField(Enemy)
episode = ForeignKeyField(Episode, backref='deaths') episode = ForeignKeyField(Episode, backref='deaths', on_delete='CASCADE')
class Meta: class Meta:
database = db database = db
@@ -85,8 +85,8 @@ class Penalty(Model):
id = AutoField() id = AutoField()
size = DecimalField() size = DecimalField()
drink = ForeignKeyField(Drink) drink = ForeignKeyField(Drink)
player = ForeignKeyField(Player, backref='penalties') player = ForeignKeyField(Player, backref='penalties', on_delete='CASCADE')
death = ForeignKeyField(Death, backref='penalties') death = ForeignKeyField(Death, backref='penalties', on_delete='CASCADE')
class Meta: class Meta:
database = db database = db
@@ -98,7 +98,7 @@ class Victory(Model):
time = TimeField(default=datetime.time(0, 0)) time = TimeField(default=datetime.time(0, 0))
player = ForeignKeyField(Player) player = ForeignKeyField(Player)
enemy = ForeignKeyField(Enemy) enemy = ForeignKeyField(Enemy)
episode = ForeignKeyField(Episode, backref='victories') episode = ForeignKeyField(Episode, backref='victories', on_delete='CASCADE')
class Meta: class Meta:
database = db 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_write import WriteFunctions
from dsst_server.func_read import ReadFunctions from dsst_server.func_read import ReadFunctions
from dsst_server.func_delete import DeleteFunctions
class FunctionProxy(WriteFunctions, ReadFunctions): class FunctionProxy(WriteFunctions, ReadFunctions, DeleteFunctions):
pass pass

View File

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

View File

@@ -1,13 +1,16 @@
from common import models from common import models
from dsst_server.data_access import sql from dsst_server.data_access import sql
from dsst_server.auth import check_write
class WriteFunctions: class WriteFunctions:
@staticmethod @staticmethod
@check_write
def create_season(season: 'models.Season'): def create_season(season: 'models.Season'):
return 'Season created.' return 'Season created.'
@staticmethod @staticmethod
@check_write
def update_enemy(enemy: 'models.Enemy', *_): def update_enemy(enemy: 'models.Enemy', *_):
(sql.Enemy (sql.Enemy
.insert(id=enemy.id, boss=enemy.boss, name=enemy.name, season=enemy.season) .insert(id=enemy.id, boss=enemy.boss, name=enemy.name, season=enemy.season)
@@ -17,6 +20,7 @@ class WriteFunctions:
.execute()) .execute())
@staticmethod @staticmethod
@check_write
def update_player(player: 'models.Player', *_): def update_player(player: 'models.Player', *_):
(sql.Player (sql.Player
.insert(id=player.id, name=player.name, hex_id=player.hex_id) .insert(id=player.id, name=player.name, hex_id=player.hex_id)
@@ -25,6 +29,7 @@ class WriteFunctions:
.execute()) .execute())
@staticmethod @staticmethod
@check_write
def update_drink(drink: 'models.Drink', *_): def update_drink(drink: 'models.Drink', *_):
(sql.Drink (sql.Drink
.insert(id=drink.id, name=drink.name, vol=drink.vol) .insert(id=drink.id, name=drink.name, vol=drink.vol)
@@ -33,6 +38,7 @@ class WriteFunctions:
.execute()) .execute())
@staticmethod @staticmethod
@check_write
def save_death(death: 'models.Death'): def save_death(death: 'models.Death'):
with sql.db.atomic(): with sql.db.atomic():
created_id = (sql.Death 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) sql.Penalty.create(death=created_id, size=penalty.size, drink=penalty.drink, player=penalty.player)
@staticmethod @staticmethod
@check_write
def save_victory(victory: 'models.Victory'): def save_victory(victory: 'models.Victory'):
(sql.Victory (sql.Victory
.insert(info=victory.info, player=victory.player, enemy=victory.enemy, time=victory.time, .insert(info=victory.info, player=victory.player, enemy=victory.enemy, time=victory.time,
@@ -50,6 +57,7 @@ class WriteFunctions:
.execute()) .execute())
@staticmethod @staticmethod
@check_write
def update_season(season: 'models.Season', *_): def update_season(season: 'models.Season', *_):
(sql.Season (sql.Season
.insert(id=season.id, number=season.number, game_name=season.game_name, start_date=season.start_date, .insert(id=season.id, number=season.number, game_name=season.game_name, start_date=season.start_date,
@@ -62,6 +70,7 @@ class WriteFunctions:
.execute()) .execute())
@staticmethod @staticmethod
@check_write
def update_episode(episode: 'models.Episode', *_): def update_episode(episode: 'models.Episode', *_):
players = list(sql.Player.select().where(sql.Player.id << [player.id for player in episode.players])) players = list(sql.Player.select().where(sql.Player.id << [player.id for player in episode.players]))
new_ep_id = (sql.Episode new_ep_id = (sql.Episode
@@ -76,3 +85,8 @@ class WriteFunctions:
db_episode = sql.Episode.get(sql.Episode.id == new_ep_id) db_episode = sql.Episode.get(sql.Episode.id == new_ep_id)
db_episode.players = players db_episode.players = players
db_episode.save() 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 json
import pickle import pickle
import socket import socket
import sys import sys
import os import os
import logging
from logging.config import dictConfig
from common import util, models from common import util
from dsst_server import func_read, func_write, tokens from dsst_server import auth
from dsst_server.func_proxy import FunctionProxy from dsst_server.func_proxy import FunctionProxy
from dsst_server.data_access import sql, sql_func from dsst_server.data_access import sql, sql_func
from dsst_server.config import DEFAULT_CONFIG
PORT = 12345
HOST = socket.gethostname()
BUFFER_SIZE = 1024
class DsstServer: 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) 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)) # Save Database credentials
print('Bound socket to {} on host {}'.format(PORT, HOST))
# Initialize database
db_config = config.get('database') db_config = config.get('database')
sql.db.init(db_config.get('db_name'), user=db_config.get('user'), password=db_config.get('password')) self.db_user = db_config.get('user')
sql_func.create_tables() self.db_name = db_config.get('db_name')
print('Database initialized ({})'.format(sql.db.database)) self.db_password = db_config.get('password')
# Load access tokens and map them to their allowed methods # Load access tokens
read_actions = util.list_class_methods(func_read.ReadFunctions) auth.READ_TOKENS = config.get('tokens').get('readonly')
write_actions = util.list_class_methods(func_write.WriteFunctions) auth.WRITE_TOKENS = config.get('tokens').get('readwrite')
parm_access = { logging.info('Auth tokens loaded')
'r': read_actions,
'rw': read_actions + write_actions def process_request(self, request: dict) -> dict:
} """ Process a requested function from a client
self.tokens = {token: parm_access[perms] for token, perms in tokens.TOKENS} :param request: Request dictionary
print('Loaded auth tokens: {}'.format(self.tokens.keys())) :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): def run(self):
self.socket_server.listen(5) self.socket_server.listen(5)
print('Socket is listening') logging.info('Socket is listening')
while True: while True:
# Accept client connection
client, address = self.socket_server.accept() client, address = self.socket_server.accept()
try: logging.info('-' * 30)
print('Connection from {}'.format(address)) logging.info('Connection from {}'.format(address))
# Parse request from client
data = util.recv_msg(client) data = util.recv_msg(client)
request = pickle.loads(data) request = pickle.loads(data)
print('Request: {}'.format(request)) logging.info('Request: {}'.format(request))
# Validate auth token in request
token = request.get('auth_token') # Process request
if token not in self.tokens: response = self.process_request(request)
util.send_msg(client, pickle.dumps({'success': False, 'message': 'Auth token invalid'}))
print('Rejected request from {}. Auth token invalid ({})'.format(address, token)) # Send data back to client
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)) util.send_msg(client, pickle.dumps(response))
raise
response = {'success': True, 'data': value} # Close connection
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() client.close()
print('Connection to client closed') logging.info('Connection to client closed')
def load_config(config_path: str) -> dict: def load_config(config_path: str) -> dict:
@@ -85,13 +104,28 @@ def load_config(config_path: str) -> dict:
return json.load(config_file) 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(): def main():
config = os.path.join(os.path.expanduser('~'), '.config', 'dsst', 'server.json') 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)) server = DsstServer(load_config(config))
try: try:
server.run() server.run()
except KeyboardInterrupt: except KeyboardInterrupt:
print('Server stopped') logging.info('Server stopped')
server.socket_server.close() server.socket_server.close()
try: try:
sys.exit(0) 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')]