Merge pull request #1 from luxick/client_server

Merge Client/Server Branch
This commit is contained in:
luxick
2018-03-14 19:45:47 +01:00
committed by GitHub
31 changed files with 2915 additions and 1639 deletions

View File

@@ -2,19 +2,33 @@
Over-engineered statistics tool for keeping track of a drinking game
## Running the application
### Build executable zip archive
Run build script
The application is split into an server and client part.
To run standalone on a single machine you will have to have a running
dsst-server that is connected to a mysql database.
`$ python3 ./build.py`
Using the client you can then connect to the server via its
specified port.
### Building
Run the build script with the desired option
The archive will be saved into the `build` folder. The file is completly standalone and can be run from anywhere.
`$ python3 ./build.py {gtk3|server|all}`
`$ ./build/dsst`
The archive(s) will be saved into the `build` folder.
To run either server or client, just execute the archive files.
### Run python script directly
`$ python3 ./dsst/__main__.py`
`$ ./build/dsst-server-0.1`
To run the server.
`$ ./build/dsst-gtk3-0.1`
To run the GTK client.
## Dependencies
- GObject (Gtk3)
- Python 3
### Client
- python-gi <= v3.16 (Gtk3)
### Server
- mysqlclient (Python Mysql Driver)
- peewee (ORM Framework)

View File

@@ -3,16 +3,60 @@ Package application using zipapp into an executable zip archive
"""
import os
import zipapp
import sys
import shutil
INTERPRETER = '/usr/bin/env python3'
SOURCE_PATH = 'dsst'
TARGET_FILENAME = 'dsst'
# The bundled file should be placed into the build directory
target_path = os.path.join(os.path.dirname(__file__), 'build')
CLIENT_VERSION = '0.1'
SERVER_VERSION = '0.1'
try:
build_mode = sys.argv[1]
except IndexError:
print('No build mode specified')
sys.exit(0)
print('Building Mode: {}'.format(build_mode))
path = os.path.dirname(__file__)
# Specify build path
BUILD_PATH = os.path.join(path, 'build')
# Make sure it exists
if not os.path.isdir(target_path):
os.mkdir(target_path)
target = os.path.join(target_path, TARGET_FILENAME)
# Create archive
zipapp.create_archive(source=SOURCE_PATH, target=target, interpreter=INTERPRETER)
if not os.path.isdir(BUILD_PATH):
os.mkdir(BUILD_PATH)
def build(target_filename, folder_name, entry_point):
source_path = os.path.join(BUILD_PATH, 'source')
if os.path.isdir(source_path):
shutil.rmtree(source_path)
os.mkdir(source_path)
shutil.copytree(os.path.join(path, 'dsst', folder_name), os.path.join(source_path, folder_name))
shutil.copytree(os.path.join(path, 'dsst', 'common'), os.path.join(source_path, 'common'))
archive_name = os.path.join(BUILD_PATH, target_filename)
zipapp.create_archive(source=source_path, target=archive_name, interpreter=INTERPRETER,
main=entry_point)
print('Created {}'.format(archive_name))
shutil.rmtree(source_path)
def build_server():
build('dsst-server-{}'.format(SERVER_VERSION), 'dsst_server', 'dsst_server.server:main')
def build_gtk3():
build('dsst-gtk3-{}'.format(CLIENT_VERSION), 'dsst_gtk3', 'dsst_gtk3.gtk_ui:main')
build_modes = {
'server': build_server,
'gtk3': build_gtk3
}
if build_mode == 'all':
for mode, build_function in build_modes.items():
build_function()
else:
build_modes[build_mode]()

85
dsst/common/models.py Normal file
View File

@@ -0,0 +1,85 @@
class Season:
def __init__(self, arg={}):
self.id = arg.get('id')
self.number = arg.get('number')
self.game_name = arg.get('game_name')
self.start_date = arg.get('start_date')
self.end_date = arg.get('end_date')
self.episodes = arg.get('episodes')
self.enemies = arg.get('enemies')
class Player:
def __init__(self, arg={}):
self.id = arg.get('id')
self.name = arg.get('name')
self.hex_id = arg.get('hex_id')
self.deaths = arg.get('deaths')
self.victories = arg.get('victories')
self.penalties = arg.get('penalties')
class Episode:
def __init__(self, arg={}):
self.id = arg.get('id')
self.seq_number = arg.get('seq_number')
self.number = arg.get('number')
self.name = arg.get('name')
self.date = arg.get('date')
self.season = arg.get('season')
self.players = arg.get('players')
self.deaths = arg.get('deaths')
self.victories = arg.get('victories')
class Drink:
def __init__(self, arg={}):
self.id = arg.get('id')
self.name = arg.get('name')
self.vol = arg.get('vol')
class Enemy:
def __init__(self, arg={}):
self.id = arg.get('id')
self.name = arg.get('name')
self.boss = arg.get('boss')
self.season = arg.get('season')
class Death:
def __init__(self, arg={}):
self.id = arg.get('id')
self.info = arg.get('info')
self.player = arg.get('player')
self.enemy = arg.get('enemy')
self.episode = arg.get('episode')
self.penalties = arg.get('penalties')
self.time = arg.get('time')
class Penalty:
def __init__(self, arg={}):
self.id = arg.get('id')
self.size = arg.get('size')
self.drink = arg.get('drink')
self.player = arg.get('player')
self.death = arg.get('death')
class Victory:
def __init__(self, arg={}):
self.id = arg.get('id')
self.info = arg.get('info')
self.player = arg.get('player')
self.enemy = arg.get('enemy')
self.episode = arg.get('episode')
self.time = arg.get('time')
class SeasonStats:
def __init__(self, arg={}):
self.player_kd = arg.get('player_kd')
self.enemies = arg.get('enemies')

32
dsst/common/util.py Normal file
View File

@@ -0,0 +1,32 @@
import struct
def send_msg(sock, msg):
# Prefix each message with a 4-byte length (network byte order)
msg = struct.pack('>I', len(msg)) + msg
sock.sendall(msg)
def recv_msg(sock):
# Read message length and unpack it into an integer
raw_msglen = recvall(sock, 4)
if not raw_msglen:
return None
msglen = struct.unpack('>I', raw_msglen)[0]
# Read the message data
return recvall(sock, msglen)
def recvall(sock, n):
# Helper function to recv n bytes or return None if EOF is hit
data = b''
while len(data) < n:
packet = sock.recv(n - len(data))
if not packet:
return None
data += packet
return data
def list_class_methods(class_obj):
return [name for name in dir(class_obj) if not name.startswith('__')]

View File

@@ -1,10 +1,11 @@
import sys
import os.path
import sys
# Add current directory to python path
path = os.path.realpath(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(path))
sys.path.insert(0, os.path.dirname(os.path.dirname(path)))
from dsst_gtk3 import gtk_ui
if __name__ == '__main__':
gtk_ui.main()
gtk_ui.main()

40
dsst/dsst_gtk3/client.py Normal file
View File

@@ -0,0 +1,40 @@
import pprint
import socket
from common import util, models
try:
import cPickle as pickle
except ImportError:
import pickle
class Access:
def __init__(self, conn_dict):
self.host = conn_dict.get('host')
self.port = conn_dict.get('port')
self.buffer = conn_dict.get('buffer_size')
self.auth_token = conn_dict.get('auth_token')
def send_request(self, action: str, *args):
request = {'auth_token': self.auth_token,
'action': action,
'args': args}
request = pickle.dumps(request)
soc = socket.socket()
try:
soc.connect((self.host, self.port))
util.send_msg(soc, request)
message = util.recv_msg(soc)
message = pickle.loads(message)
if not message.get('success'):
raise Exception(message.get('message'))
finally:
soc.close()
return message.get('data')
if __name__ == '__main__':
access = Access({'host': 'europa', 'port': 12345, 'buffer_size': 1024, 'auth_token': 'a'})
action = 'load_seasons'
response = access.send_request(action)
pp = pprint.PrettyPrinter(indent=1)
for s in response:
pp.pprint(s.__dict__)

View File

@@ -1,12 +1,10 @@
"""
This module contains UI functions for displaying different dialogs
"""
import gi
gi.require_version('Gtk', '3.0')
import datetime
from gi.repository import Gtk
from datetime import datetime
from dsst_sql import sql
from dsst_gtk3 import util
from common import models
from dsst_gtk3 import gtk_ui, util
def enter_string_dialog(builder: Gtk.Builder, title: str, value=None) -> str:
@@ -33,159 +31,122 @@ def enter_string_dialog(builder: Gtk.Builder, title: str, value=None) -> str:
return value
def show_episode_dialog(builder: Gtk.Builder, title: str, season_id: int, episode: sql.Episode=None):
""" Shows a dialog to edit an episode
:param builder: GtkBuilder with loaded 'dialogs.glade'
:param title: Title of the dialog window
:param season_id: Season to witch the episode should be added
:param episode: (Optional) Existing episode to edit
:return True if changes where saved False if discarded
"""
# Set up the dialog
dialog = builder.get_object("edit_episode_dialog") # type: Gtk.Dialog
dialog.set_transient_for(builder.get_object("main_window"))
dialog.set_title(title)
with sql.db.atomic():
if not episode:
nxt_number = len(sql.Season.get_by_id(season_id).episodes) + 1
episode = sql.Episode.create(seq_number=nxt_number, number=nxt_number, date=datetime.today(),
season=season_id)
# Set episode number
builder.get_object("episode_no_spin_button").set_value(episode.number)
# Set episode date
builder.get_object('episode_calendar').select_month(episode.date.month, episode.date.year)
builder.get_object('episode_calendar').select_day(episode.date.day)
# Set participants for the episode
builder.get_object('episode_players_store').clear()
for player in episode.players:
builder.get_object('episode_players_store').append([player.id, player.name, player.hex_id])
def edit_season(builder: 'Gtk.Builder', season: 'models.Season'=None):
if not season:
season = models.Season()
builder.get_object('season_number_spin').set_value(season.number or 1)
builder.get_object('season_game_entry').set_text(season.game_name or '')
builder.get_object('season_start_entry').set_text(season.start_date or '')
builder.get_object('season_end_entry').set_text(season.end_date or '')
result = dialog.run()
dialog.hide()
if result != Gtk.ResponseType.OK:
sql.db.rollback()
return False
# Save all changes to Database
player_ids = [row[0] for row in builder.get_object('episode_players_store')]
# Insert new Players
episode.players = sql.Player.select().where(sql.Player.id << player_ids)
# Update Date of the Episode
cal_value = builder.get_object('episode_calendar').get_date()
selected_date = datetime(*cal_value).date()
episode.date = selected_date,
episode.number = int(builder.get_object("episode_no_spin_button").get_value())
episode.name = builder.get_object("episode_name_entry").get_text()
episode.save()
return True
def show_manage_players_dialog(builder: Gtk.Builder, title: str):
"""Show a dialog for managing player base data.
:param builder: Gtk.Builder object
:param title: Title for the dialog
"""
dialog = builder.get_object("manage_players_dialog") # type: Gtk.Dialog
dialog.set_transient_for(builder.get_object("main_window"))
dialog.run()
dialog = builder.get_object('edit_season_dialog')
result = dialog.run()
dialog.hide()
if result != Gtk.ResponseType.OK:
return None
def show_manage_enemies_dialog(builder: Gtk.Builder, season_id: int):
dialog = builder.get_object("manage_enemies_dialog") # type: Gtk.Dialog
dialog.set_transient_for(builder.get_object("main_window"))
dialog.run()
season.number = builder.get_object('season_number_spin').get_value()
season.game_name = builder.get_object('season_game_entry').get_text()
start_string = builder.get_object('season_start_entry').get_text()
if start_string:
season.start_date = datetime.datetime.strptime(start_string, '%Y-%m-%d')
end_string = builder.get_object('season_end_entry').get_text()
if end_string:
season.end_date = datetime.datetime.strptime(end_string, '%Y-%m-%d')
return season
def edit_episode(app: 'gtk_ui.GtkUi', season_id: int, episode: 'models.Episode'=None):
"""Show an dialog to create or edit episodes
:param app: Reference to main UI application
:param season_id: Is of the season in which the episode appears
:param episode: Existing episode object to edit
:return: Edited episode object, or None if the process was canceled
"""
if not episode:
episode = models.Episode()
episode.date = datetime.datetime.today()
episode.number = 1
episode.name = ''
episode.players = []
app.ui.get_object('episode_name_entry').set_text(episode.name)
app.ui.get_object('episode_no_spin_button').set_value(episode.number)
app.ui.get_object('episode_calendar').select_month(episode.date.month, episode.date.year)
app.ui.get_object('episode_calendar').select_day(episode.date.day)
app.ui.get_object('episode_players_store').clear()
for player in episode.players:
app.ui.get_object('episode_players_store').append([player.id, player.name, player.hex_id])
dialog = app.ui.get_object('edit_episode_dialog') # type: Gtk.Dialog
result = dialog.run()
dialog.hide()
if result != Gtk.ResponseType.OK:
return None
def show_manage_drinks_dialog(builder: Gtk.Builder):
dialog = builder.get_object("manage_drinks_dialog") # type: Gtk.Dialog
dialog.set_transient_for(builder.get_object("main_window"))
dialog.run()
episode.name = app.ui.get_object('episode_name_entry').get_text()
episode.number = app.ui.get_object('episode_no_spin_button').get_value()
cal_value = app.ui.get_object('episode_calendar').get_date()
selected_date = datetime.datetime(*cal_value).date()
episode.date = selected_date
player_ids = [row[0] for row in app.ui.get_object('episode_players_store')]
episode.players = [app.get_by_id(app.players, player_id) for player_id in player_ids]
episode.season = season_id
return episode
def create_death(app: 'gtk_ui.GtkUi'):
"""Show a dialog to create death events for an episode
:param app: Main Gtk application
:return: Death object or None if dialog was canceled
"""
# Run the dialog
dialog = app.ui.get_object("edit_death_dialog") # type: Gtk.Dialog
result = dialog.run()
dialog.hide()
if result != Gtk.ResponseType.OK:
return None
death = models.Death()
hour_spin = app.ui.get_object('death_hour_spin')
min_spin = app.ui.get_object('death_min_spin')
# Parse the inputs
death.time = datetime.time(int(hour_spin.get_value()), int(min_spin.get_value()))
death.enemy = util.get_combo_value(app.ui.get_object('edit_death_enemy_combo'), 4)
death.player = util.get_combo_value(app.ui.get_object('edit_death_player_combo'), 0)
death.info = app.ui.get_object('edit_death_comment_entry').get_text()
death.episode = app.get_selected_episode_id()
store = app.ui.get_object('player_penalties_store')
size = app.ui.get_object('edit_death_size_spin').get_value()
death.penalties = []
for entry in store:
drink_id = [drink.id for drink in app.drinks.data if drink.name == entry[2]][0]
penalty = models.Penalty({'id': entry[0], 'size': size, 'drink': drink_id, 'player': entry[3]})
death.penalties.append(penalty)
return death
def show_edit_death_dialog(builder: Gtk.Builder, episode_id: int, death: sql.Death=None):
"""Show a dialog for editing or creating death events.
:param builder: A Gtk.Builder object
:param episode_id: ID to witch the death event belongs to
:param death: (Optional) Death event witch should be edited
:return: Gtk.ResponseType of the dialog
def create_victory(app: 'gtk_ui.GtkUi'):
"""Show a dialog for creating victory events
:param app: Reference to main gtk ui object
:return: Created victory object or None, if canceled
"""
dialog = builder.get_object("edit_death_dialog") # type: Gtk.Dialog
dialog.set_transient_for(builder.get_object("main_window"))
with sql.db.atomic():
if death:
index = util.get_index_of_combo_model(builder.get_object('edit_death_enemy_combo'), 0, death.enemy.id)
builder.get_object('edit_death_enemy_combo').set_active(index)
dialog = app.ui.get_object('edit_victory_dialog')
result = dialog.run()
dialog.hide()
if result != Gtk.ResponseType.OK:
return None
# TODO Default drink should be set in config
default_drink = sql.Drink.get().name
store = builder.get_object('player_penalties_store')
store.clear()
for player in builder.get_object('episode_players_store'):
store.append([None, player[1], default_drink, player[0]])
hour_spin = app.ui.get_object('vic_hour_spin')
min_spin = app.ui.get_object('vic_min_spin')
victory = models.Victory()
victory.episode = app.get_selected_episode_id()
victory.info = app.ui.get_object('victory_comment_entry').get_text()
victory.player = util.get_combo_value(app.ui.get_object('edit_victory_player_combo'), 0)
victory.enemy = util.get_combo_value(app.ui.get_object('edit_victory_enemy_combo'), 4)
victory.time = datetime.time(int(hour_spin.get_value()), int(min_spin.get_value()))
# Run the dialog
result = dialog.run()
dialog.hide()
if result != Gtk.ResponseType.OK:
sql.db.rollback()
return result
# Collect info from widgets and save to database
player_id = util.get_combo_value(builder.get_object('edit_death_player_combo'), 0)
enemy_id = util.get_combo_value(builder.get_object('edit_death_enemy_combo'), 3)
comment = builder.get_object('edit_death_comment_entry').get_text()
if not death:
death = sql.Death.create(episode=episode_id, player=player_id, enemy=enemy_id, info=comment)
store = builder.get_object('player_penalties_store')
size = builder.get_object('edit_death_size_spin').get_value()
for entry in store:
drink_id = sql.Drink.get(sql.Drink.name == entry[2])
sql.Penalty.create(size=size, player=entry[3], death=death.id, drink=drink_id)
return result
def show_edit_victory_dialog(builder: Gtk.Builder, episode_id: int, victory: sql.Victory=None):
"""Show a dialog for editing or creating victory events.
:param builder: A Gtk.Builder object
:param episode_id: ID to witch the victory event belongs to
:param victory: (Optional) Victory event witch should be edited
:return: Gtk.ResponseType of the dialog
"""
dialog = builder.get_object("edit_victory_dialog") # type: Gtk.Dialog
dialog.set_transient_for(builder.get_object("main_window"))
with sql.db.atomic():
if victory:
infos = [['edit_victory_player_combo', victory.player.id],
['edit_victory_enemy_combo', victory.enemy.id]]
for info in infos:
combo = builder.get_object(info[0])
index = util.get_index_of_combo_model(combo, 0, info[1])
combo.set_active(index)
builder.get_object('victory_comment_entry').set_text(victory.info)
# Run the dialog
result = dialog.run()
dialog.hide()
if result != Gtk.ResponseType.OK:
sql.db.rollback()
return result
# Collect info from widgets and save to database
player_id = util.get_combo_value(builder.get_object('edit_victory_player_combo'), 0)
enemy_id = util.get_combo_value(builder.get_object('edit_victory_enemy_combo'), 3)
comment = builder.get_object('victory_comment_entry').get_text()
if not victory:
sql.Victory.create(episode=episode_id, player=player_id, enemy=enemy_id, info=comment)
else:
victory.player = player_id
victory.enemy = enemy_id
victory.info = comment
victory.save()
return result
return victory

View File

@@ -1,11 +1,10 @@
import gi
import os
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from dsst_gtk3.handlers import handlers
from dsst_gtk3 import util, reload
from dsst_sql import sql, sql_func
from dsst_gtk3 import util, reload, client
from common import models
class GtkUi:
""" The main UI class """
@@ -18,36 +17,90 @@ class GtkUi:
]
for path in glade_resources:
self.ui.add_from_string(util.load_ui_resource_string(path))
# Set the status bar logo
dd_logo = ['dsst_gtk3', 'resources', 'images', 'dd.png']
logo_pixbuf = util.load_image_resource(dd_logo, 60, 13)
logo = self.ui.get_object('status_bar_logo').set_from_pixbuf(logo_pixbuf)
# Connect signal handlers to UI
self.handlers = handlers.Handlers(self)
self.ui.connect_signals(self.handlers)
# Show all widgets
self.ui.get_object('main_window').show_all()
db_config = config['sql_connections'][0]
# Initialize the database
sql.db.init(db_config['db_name'], host=db_config['host'], port=db_config['port'],
user=db_config['user'], password=db_config['password'])
# Show database info in status bar
self.set_db_status_label(db_config)
# Create database if not exists
sql_func.create_tables()
self.reload()
# Connect to data server
config = config['servers'][0]
self.data_client = client.Access(config)
# Create local data caches
self.players = util.Cache()
self.drinks = util.Cache()
self.seasons = util.Cache()
self.episodes = util.Cache()
self.enemies = util.Cache()
self.season_stats = util.Cache()
# Create meta data cache
self.meta = {'connection': '{}:{}'.format(config.get('host'), config.get('port'))}
# Load base data and seasons
self.load_server_meta()
self.full_reload()
self.update_status_bar_meta()
def load_server_meta(self):
self.meta['database'] = self.data_client.send_request('load_db_meta')
def full_reload(self):
with util.network_operation(self):
self.players.data = self.data_client.send_request('load_players')
self.drinks.data = self.data_client.send_request('load_drinks')
self.seasons.data = self.data_client.send_request('load_seasons')
season_id = self.get_selected_season_id()
if season_id:
self.episodes.data = self.data_client.send_request('load_episodes', season_id)
self.season_stats.data = self.data_client.send_request('load_season_stats', season_id)
cur_season = [s for s in self.seasons.data if s.id == season_id][0]
self.enemies.data = cur_season.enemies
reload.rebuild_view_data(self)
def reload(self):
reload.reload_base_data(self.ui, self)
season_id = self.get_selected_season_id()
if season_id:
reload.reload_episodes(self.ui, self, season_id)
reload.reload_season_stats(self.ui, self, season_id)
else:
return
episode_id = self.get_selected_episode_id()
if episode_id:
reload.reload_episode_stats(self.ui, self, episode_id)
pass
def set_db_status_label(self, db_conf: dict):
self.ui.get_object('connection_label').set_text(f'{db_conf["user"]}@{db_conf["host"]}')
self.ui.get_object('db_label').set_text(f'{db_conf["db_name"]}')
def update_enemy(self, enemy: 'models.Enemy'):
with util.network_operation(self):
self.data_client.send_request('update_enemy', enemy)
self.full_reload()
def update_player(self, player: 'models.Player'):
with util.network_operation(self):
self.data_client.send_request('update_player', player)
self.full_reload()
def update_drink(self, drink: 'models.Drink'):
with util.network_operation(self):
self.data_client.send_request('update_drink', drink)
self.full_reload()
def save_death(self, death: 'models.Death'):
with util.network_operation(self):
self.data_client.send_request('save_death', death)
self.full_reload()
def save_victory(self, victory: 'models.Victory'):
with util.network_operation(self):
self.data_client.send_request('save_victory', victory)
self.full_reload()
def update_season(self, season: 'models.Season'):
with util.network_operation(self):
self.data_client.send_request('update_season', season)
self.seasons.valid = False
def update_episode(self, episode: 'models.Episode'):
with util.network_operation(self):
self.data_client.send_request('update_episode', episode)
self.episodes.valid = False
self.season_stats.valid = False
def update_status_bar_meta(self):
self.ui.get_object('connection_label').set_text(self.meta.get('connection'))
self.ui.get_object('db_label').set_text(self.meta.get('database') or '')
def get_selected_season_id(self) -> int:
"""Read ID of the selected season from the UI
@@ -63,6 +116,13 @@ class GtkUi:
(model, tree_iter) = self.ui.get_object('episodes_tree_view').get_selection().get_selected()
return model.get_value(tree_iter, 0) if tree_iter else None
@staticmethod
def get_by_id(cache: 'util.Cache', object_id: int):
try:
return [x for x in cache.data if x.id == object_id][0]
except KeyError:
return None
def main():
if not os.path.isfile(util.CONFIG_PATH):
@@ -70,3 +130,7 @@ def main():
config = util.load_config(util.CONFIG_PATH)
GtkUi(config)
Gtk.main()
if __name__ == '__main__':
main()

View File

@@ -1,5 +1,5 @@
from dsst_gtk3 import dialogs, gtk_ui
from dsst_sql import sql
from common import models
class BaseDataHandlers:
@@ -7,48 +7,39 @@ class BaseDataHandlers:
def __init__(self, app: 'gtk_ui.GtkUi'):
self.app = app
def do_manage_players(self, *_):
dialogs.show_manage_players_dialog(self.app.ui, 'Manage Players')
def do_add_player(self, entry):
if entry.get_text():
sql.Player.create(name=entry.get_text())
self.app.update_player(models.Player({'name': entry.get_text()}))
entry.set_text('')
self.app.reload()
def do_manage_enemies(self, *_):
dialogs.show_manage_enemies_dialog(self.app.ui, self.app.get_selected_season_id())
def on_player_name_edited(self, _, index, value):
row = self.app.ui.get_object('all_players_store')[index]
sql.Player.update(name=value)\
.where(sql.Player.id == row[0])\
.execute()
self.app.reload()
player = models.Player({'id': row[0],
'name': value,
'hex_id': row[2]})
self.app.update_player(player)
def on_player_hex_edited(self, _, index, value):
row = self.app.ui.get_object('all_players_store')[index]
sql.Player.update(hex_id=value)\
.where(sql.Player.id == row[0])\
.execute()
self.app.reload()
player = models.Player({'id': row[0],
'name': row[1],
'hex_id': value})
self.app.update_player(player)
def do_add_drink(self, entry):
if entry.get_text():
sql.Drink.create(name=entry.get_text(), vol=0)
drink = models.Drink({'name': entry.get_text(), 'vol': 0.00})
self.app.update_drink(drink)
entry.set_text('')
self.app.reload()
def on_drink_name_edited(self, _, index, value):
row = self.app.ui.get_object('drink_store')[index]
sql.Drink.update(name=value)\
.where(sql.Drink.id == row[0])\
.execute()
self.app.reload()
drink = [d for d in self.app.drinks.data if d.id == row[0]][0]
drink.name = value
self.app.update_drink(drink)
def on_drink_vol_edited(self, _, index, value):
row = self.app.ui.get_object('drink_store')[index]
sql.Drink.update(vol=value) \
.where(sql.Drink.id == row[0]) \
.execute()
self.app.reload()
drink = [d for d in self.app.drinks.data if d.id == row[0]][0]
drink.vol = value
self.app.update_drink(drink)

View File

@@ -11,9 +11,9 @@ class DeathHandlers:
ep_id = self.app.get_selected_episode_id()
if not ep_id:
return
result = dialogs.show_edit_death_dialog(self.app.ui, ep_id)
if result == Gtk.ResponseType.OK:
self.app.reload()
death = dialogs.create_death(self.app)
if death:
self.app.save_death(death)
def on_penalty_drink_changed(self, _, path, text):
self.app.ui.get_object('player_penalties_store')[path][2] = text

View File

@@ -1,5 +1,8 @@
from dsst_gtk3 import dialogs, util, gtk_ui
from dsst_sql import sql
import datetime
from dsst_gtk3 import dialogs, util, gtk_ui, reload
from common import models
from gi.repository import Gtk
class DialogHandlers:
@@ -7,6 +10,11 @@ class DialogHandlers:
def __init__(self, app: 'gtk_ui.GtkUi'):
self.app = app
@staticmethod
def do_run_manage_dialog(dialog: 'Gtk.Dialog'):
dialog.run()
dialog.hide()
def do_add_player_to_episode(self, combo):
""" Signal Handler for Add Player to Episode Button in Manage Episode Dialog
:param combo: Combo box with all the available players
@@ -14,7 +22,7 @@ class DialogHandlers:
player_id = util.get_combo_value(combo, 0)
if player_id:
self.app.ui.get_object('add_player_combo_box').set_active(-1)
player = sql.Player.get(sql.Player.id == player_id)
player = self.app.get_by_id(self.app.players, player_id)
store = self.app.ui.get_object('episode_players_store')
if not any(row[0] == player_id for row in store):
store.append([player_id, player.name, player.hex_id])
@@ -22,9 +30,41 @@ class DialogHandlers:
def do_add_enemy(self, entry):
if entry.get_text():
store = self.app.ui.get_object('enemy_season_store')
enemy = sql.Enemy.create(name=entry.get_text(), season=self.app.get_selected_season_id())
store.append([enemy.name, False, 0, enemy.id])
enemy = models.Enemy()
enemy.name = entry.get_text()
enemy.season = self.app.get_selected_season_id()
enemy.boss = not self.app.ui.get_object('enemy_optional_ckeck').get_active()
self.app.ui.get_object('enemy_optional_ckeck').set_active(False)
entry.set_text('')
def do_manage_drinks(self, *_):
result = dialogs.show_manage_drinks_dialog(self.app.ui)
self.app.update_enemy(enemy)
def on_enemy_name_edited(self, _, index, value):
row = self.app.ui.get_object('enemy_season_store')[index]
enemy = [enemy for enemy in self.app.enemies.data if enemy.id == row[4]][0]
enemy.name = value
self.app.update_enemy(enemy)
def on_enemy_optional_edited(self, renderer, index):
new_optional_value = not renderer.get_active()
row = self.app.ui.get_object('enemy_season_store')[index]
enemy = [enemy for enemy in self.app.enemies.data if enemy.id == row[4]][0]
enemy.boss = new_optional_value
self.app.update_enemy(enemy)
def do_show_date_picker(self, entry: 'Gtk.Entry', *_):
dialog = self.app.ui.get_object('date_picker_dialog')
result = dialog.run()
dialog.hide()
if result == Gtk.ResponseType.OK:
date = self.app.ui.get_object('date_picker_calendar').get_date()
date_string = '{}-{:02d}-{:02d}'.format(date.year, date.month +1, date.day)
entry.set_text(date_string)
@staticmethod
def do_set_today(cal: 'Gtk.Calendar'):
"""Set date of a Gtk Calendar to today
:param cal: Gtk.Calendar
"""
cal.select_month = datetime.date.today().month
cal.select_day = datetime.date.today().day

View File

@@ -6,7 +6,6 @@ from dsst_gtk3.handlers.base_data_handlers import BaseDataHandlers
from dsst_gtk3.handlers.dialog_handlers import DialogHandlers
from dsst_gtk3.handlers.death_handlers import DeathHandlers
from dsst_gtk3.handlers.victory_handlers import VictoryHandlers
from dsst_sql import sql, sql_func
class Handlers(SeasonHandlers, BaseDataHandlers, DialogHandlers, DeathHandlers, VictoryHandlers):
@@ -28,12 +27,10 @@ class Handlers(SeasonHandlers, BaseDataHandlers, DialogHandlers, DeathHandlers,
""" Signal will be sent when app should close
:param _: Arguments to the delete event
"""
sql.db.close()
Gtk.main_quit()
# DEBUG Functions ##################################################################################################
@staticmethod
def do_delete_database(*_):
sql_func.drop_tables()
sql_func.create_tables()
pass

View File

@@ -1,5 +1,4 @@
from dsst_sql import sql
from dsst_gtk3 import dialogs, gtk_ui
from dsst_gtk3 import dialogs, gtk_ui, reload
class SeasonHandlers:
@@ -8,23 +7,27 @@ class SeasonHandlers:
self.app = app
def do_add_season(self, *_):
name = dialogs.enter_string_dialog(self.app.ui, 'Name for the new Season')
if name:
sql.Season.create(game_name=name, number=1)
self.app.reload()
season = dialogs.edit_season(self.app.ui)
if season:
self.app.update_season(season)
self.app.full_reload()
def do_season_selected(self, *_):
self.app.reload()
self.app.episodes.valid = False
self.app.season_stats.valid = False
self.app.full_reload()
def do_add_episode(self, *_):
season_id = self.app.get_selected_season_id()
if not season_id:
return
dialogs.show_episode_dialog(self.app.ui, 'Create new Episode', season_id)
self.app.reload()
ep = dialogs.edit_episode(self.app, season_id)
if ep:
self.app.update_episode(ep)
self.app.full_reload()
def on_selected_episode_changed(self, *_):
self.app.reload()
reload.reload_episode_stats(self.app)
def on_episode_double_click(self, *_):
self.app.ui.get_object('stats_notebook').set_current_page(1)

View File

@@ -11,6 +11,6 @@ class VictoryHandlers:
ep_id = self.app.get_selected_episode_id()
if not ep_id:
return
result = dialogs.show_edit_victory_dialog(self.app.ui, ep_id)
if result == Gtk.ResponseType.OK:
self.app.reload()
victory = dialogs.create_victory(self.app)
if victory:
self.app.save_victory(victory)

View File

@@ -1,116 +1,123 @@
from collections import Counter
from gi.repository import Gtk
from dsst_gtk3 import gtk_ui
from dsst_sql import sql, sql_func
from dsst_gtk3 import util
from dsst_gtk3 import util, gtk_ui
def reload_base_data(builder: Gtk.Builder, app: 'gtk_ui.GtkUi'):
def reload_base_data(app: 'gtk_ui.GtkUi',):
"""Reload function for all base data witch is not dependant on a selected season or episode
:param app: GtkUi instance
:param builder: Gtk.Builder with loaded UI
"""
# Rebuild all players store
builder.get_object('all_players_store').clear()
for player in sql.Player.select():
builder.get_object('all_players_store').append([player.id, player.name, player.hex_id])
app.ui.get_object('all_players_store').clear()
for player in app.players.data:
app.ui.get_object('all_players_store').append([player.id, player.name, player.hex_id])
# Rebuild drink store
builder.get_object('drink_store').clear()
for drink in sql.Drink.select():
builder.get_object('drink_store').append([drink.id, drink.name, '{:.2f}%'.format(drink.vol)])
app.ui.get_object('drink_store').clear()
for drink in app.drinks.data:
app.ui.get_object('drink_store').append([drink.id, drink.name, '{:.2f}%'.format(drink.vol)])
# Rebuild seasons store
combo = builder.get_object('season_combo_box') # type: Gtk.ComboBox
combo = app.ui.get_object('season_combo_box') # type: Gtk.ComboBox
active = combo.get_active()
with util.block_handler(combo, app.handlers.do_season_selected):
store = builder.get_object('seasons_store')
store = app.ui.get_object('seasons_store')
store.clear()
for season in sql.Season.select().order_by(sql.Season.number):
for season in app.seasons.data:
store.append([season.id, season.game_name])
combo.set_active(active)
def reload_episodes(builder: Gtk.Builder, app: 'gtk_ui.GtkUi', season_id: int):
def reload_episodes(app: 'gtk_ui.GtkUi'):
"""Reload all data that is dependant on a selected season
:param app: GtkUi instance
:param builder: Gtk.Builder with loaded UI
:param season_id: ID of the season for witch to load data
"""
# Rebuild episodes store
selection = builder.get_object('episodes_tree_view').get_selection()
if not app.get_selected_season_id(): return
selection = app.ui.get_object('episodes_tree_view').get_selection()
with util.block_handler(selection, app.handlers.on_selected_episode_changed):
model, selected_paths = selection.get_selected_rows()
model.clear()
for episode in sql_func.get_episodes_for_season(season_id):
for episode in app.episodes.data:
model.append([episode.id, episode.name, str(episode.date), episode.number])
if selected_paths:
selection.select_path(selected_paths[0])
def reload_season_stats(builder: Gtk.Builder, app: 'gtk_ui.GtkUi', season_id: int):
def reload_season_stats(app: 'gtk_ui.GtkUi'):
"""Load statistic data for selected season
:param builder: Gtk.Builder with loaded UI
:param app: GtkUi instance
:param season_id: ID of the season for witch to load data
"""
player_stats = {}
for episode in sql_func.get_episodes_for_season(season_id):
for player in episode.players:
player_stats[player.name] = [sql_func.get_player_deaths_for_season(season_id, player.id),
sql_func.get_player_victories_for_season(season_id, player.id)]
store = builder.get_object('player_season_store')
if not app.season_stats.valid: return
season_stats = app.season_stats.data
# Load player kill/death data
store = app.ui.get_object('player_season_store')
store.clear()
for name, stats in player_stats.items():
store.append([name, stats[0], stats[1]])
for player_name, kills, deaths in season_stats.player_kd:
store.append([player_name, deaths, kills])
# Load enemy stats for season
season = sql.Season.get(sql.Season.id == season_id)
enemy_stats = {
enemy.name: [False, len(sql.Death.select().where(sql.Death.enemy == enemy)), enemy.id]
for enemy in season.enemies}
store = builder.get_object('enemy_season_store')
store = app.ui.get_object('enemy_season_store')
store.clear()
for name, stats in enemy_stats.items():
store.append([name, stats[0], stats[1], stats[2]])
for enemy_id, enemy_name, deaths, defeated, boss in season_stats.enemies:
store.append([enemy_name, defeated, deaths, boss, enemy_id])
def reload_episode_stats(builder: Gtk.Builder, app: 'gtk_ui.GtkUi', episode_id: int):
def reload_episode_stats(app: 'gtk_ui.GtkUi'):
"""Reload all data that is dependant on a selected episode
:param builder: builder: Gtk.Builder with loaded UI
:param app: app: GtkUi instance
:param episode_id: ID of the episode for witch to load data
"""
episode = sql.Episode.get(sql.Episode.id == episode_id)
store = builder.get_object('episode_players_store')
ep_id = app.get_selected_episode_id()
if not app.episodes.valid or not ep_id: return
episode = [ep for ep in app.episodes.data if ep.id == ep_id][0]
store = app.ui.get_object('episode_players_store')
store.clear()
for player in episode.players:
store.append([player.id, player.name, player.hex_id])
# Reload death store for notebook view
store = builder.get_object('episode_deaths_store')
store = app.ui.get_object('episode_deaths_store')
store.clear()
for death in episode.deaths:
penalties = [x.drink.name for x in death.penalties]
penalties = [f'{number}x {drink}' for drink, number in Counter(penalties).items()]
penalties = ['{}x {}'.format(number, drink) for drink, number in Counter(penalties).items()]
penalty_string = ', '.join(penalties)
store.append([death.id, death.player.name, death.enemy.name, penalty_string])
time_string = '{:02d}:{:02d}'.format(death.time.hour, death.time.minute)
store.append([death.id, death.player.name, death.enemy.name, penalty_string, time_string])
# Reload victory store for notebook view
store = builder.get_object('episode_victories_store')
store = app.ui.get_object('episode_victories_store')
store.clear()
for victory in episode.victories:
store.append([victory.id, victory.player.name, victory.enemy.name, victory.info])
time_string = '{:02d}:{:02d}'.format(victory.time.hour, victory.time.minute)
store.append([victory.id, victory.player.name, victory.enemy.name, victory.info, time_string])
# Stat grid
builder.get_object('ep_stat_title').set_text('Stats for episode {}\n{}'.format(episode.number, episode.name))
builder.get_object('ep_death_count_label').set_text(str(len(episode.deaths)))
app.ui.get_object('ep_stat_title').set_text('Stats for episode {}\n{}'.format(episode.number, episode.name))
app.ui.get_object('ep_death_count_label').set_text(str(len(episode.deaths)))
drink_count = sum(len(death.penalties) for death in episode.deaths)
builder.get_object('ep_drinks_label').set_text(str(drink_count))
builder.get_object('ep_player_drinks_label').set_text(str(len(episode.deaths)))
app.ui.get_object('ep_drinks_label').set_text(str(drink_count))
app.ui.get_object('ep_player_drinks_label').set_text(str(len(episode.deaths)))
dl_booze = sum(len(death.penalties) * death.penalties[0].size for death in episode.deaths)
l_booze = round(dl_booze / 10, 2)
builder.get_object('ep_booze_label').set_text('{}l'.format(l_booze))
app.ui.get_object('ep_booze_label').set_text('{}l'.format(l_booze))
dl_booze = sum(len(death.penalties) * death.penalties[0].size for death in episode.deaths)
ml_booze = round(dl_booze * 10, 0)
builder.get_object('ep_player_booze_label').set_text('{}ml'.format(ml_booze))
app.ui.get_object('ep_player_booze_label').set_text('{}ml'.format(ml_booze))
enemy_list = [death.enemy.name for death in episode.deaths]
sorted_list = Counter(enemy_list).most_common(1)
if sorted_list:
enemy_name, deaths = sorted_list[0]
builder.get_object('ep_enemy_name_label').set_text(f'{enemy_name} ({deaths} Deaths)')
app.ui.get_object('ep_enemy_name_label').set_text('{} ({} Deaths)'.format(enemy_name, deaths))
def fill_list_store(store: Gtk.ListStore, models: list):
store.clear()
for model in models:
pass
def rebuild_view_data(app: 'gtk_ui.GtkUi'):
reload_base_data(app)
reload_episodes(app)
reload_episode_stats(app)
reload_season_stats(app)

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.3 -->
<!-- Generated with glade 3.20.4 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<requires lib="gtk+" version="3.16"/>
<object class="GtkDialog" id="nameEnterDialog">
<property name="can_focus">False</property>
<property name="resizable">False</property>

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -4,23 +4,38 @@ This modules contains general utilities for the GTK application to use.
import json
import os
from contextlib import contextmanager
from gi.repository import Gtk
from gi.repository import Gtk, GdkPixbuf
from typing import Callable
from dsst_gtk3 import gtk_ui
from zipfile import ZipFile
CONFIG_PATH = os.path.join(os.path.expanduser('~'), '.config', 'dsst', 'config.json')
DEFAULT_CONFIG = {
'auto_connect': False,
'sql_connections': [{
'servers': [{
'host': 'localhost',
'port': 3306,
'db_name': 'dsst',
'user': 'dsst',
'password': 'dsst'}
'port': 12345,
'buffer_size': 1024,
'auth_token': ''}
]
}
class Cache:
def __init__(self, data={}, valid=False):
self._data = data
self.valid = valid
@property
def data(self):
return self._data
@data.setter
def data(self, value):
self._data = value
self.valid = True
@contextmanager
def block_handler(widget: 'Gtk.Widget', handler_func: Callable):
"""Run an operation while a signal handler for a widget is blocked
@@ -32,6 +47,21 @@ def block_handler(widget: 'Gtk.Widget', handler_func: Callable):
widget.handler_unblock_by_func(handler_func)
@contextmanager
def network_operation(app: 'gtk_ui.GtkUi'):
"""Run operation in try/except block and display exception in a dialog
:param app: Reference to main Gtk Application
"""
app.ui.get_object('status_bar').push(0, 'Connecting to server')
try:
yield
except Exception as e:
print(e)
app.ui.get_object('status_bar').push(0, str(e))
else:
app.ui.get_object('status_bar').push(0, '')
def get_combo_value(combo, index: int):
""" Retrieve the selected value of a combo box at the selected index in the model
:param combo: Any Gtk Widget that supports 'get_active_iter()'
@@ -59,7 +89,7 @@ def get_index_of_combo_model(widget, column: int, value: int):
def load_ui_resource_from_file(resource_path: list) -> str:
project_base_dir = os.path.dirname(os.path.dirname(__file__))
full_path = os.path.join(project_base_dir, *resource_path)
with open(full_path, 'r') as file:
with open(full_path, 'r', encoding='utf8') as file:
return file.read()
@@ -77,10 +107,35 @@ def load_ui_resource_string(resource_path: list) -> str:
if os.path.isdir(os.path.dirname(__file__)):
return load_ui_resource_from_file(resource_path)
else:
return load_ui_resource_from_archive(resource_path)
def load_image_resource_file(resource_path: list, width: int, height: int) -> GdkPixbuf:
project_base_dir = os.path.dirname(os.path.dirname(__file__))
full_path = os.path.join(project_base_dir, *resource_path)
return GdkPixbuf.Pixbuf.new_from_file_at_scale(full_path, width=width, height=height, preserve_aspect_ratio=False)
def load_image_resource_archive(resource_path: list, width: int, height: int) -> GdkPixbuf:
resource_path = os.path.join(*resource_path)
zip_path = os.path.dirname(os.path.dirname(__file__))
with ZipFile(zip_path, 'r') as archive:
with archive.open(resource_path) as data:
loader = GdkPixbuf.PixbufLoader()
loader.write(data.read())
pixbuf = loader.get_pixbuf() # type: GdkPixbuf.Pixbuf
pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
loader.close()
return pixbuf
def load_image_resource(resource_path: list, width: int, height: int) -> GdkPixbuf:
if os.path.isdir(os.path.dirname(__file__)):
return load_image_resource_file(resource_path, width, height)
else:
return load_image_resource_archive(resource_path, width, height)
def load_config(config_path: str) -> dict:
with open(config_path) as config_file:
return json.load(config_file)

View File

View File

@@ -0,0 +1,11 @@
import os.path
import sys
# Add current directory to python path
path = os.path.realpath(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(os.path.dirname(path)))
from dsst_server import server
if __name__ == '__main__':
server.main()

View File

View File

@@ -0,0 +1,70 @@
from dsst_server.data_access import sql
from common import models
def map_base_fields(cls, db_model):
"""Automatically map fields of db models to common models
:param cls: common.models class to create
:param db_model: database model from which to map
:return: An common.models object
"""
model = cls()
attrs = [attr for attr in db_model._meta.fields]
for attr in attrs:
db_attr = getattr(db_model, attr)
# Check if the attribute is an relation to another db model
# In that case just take its id
if hasattr(db_attr, 'id'):
setattr(model, attr, getattr(db_attr, 'id'))
else:
setattr(model, attr, getattr(db_model, attr))
return model
def db_to_drink(drink: 'sql.Drink'):
return map_base_fields(models.Drink, drink)
def db_to_enemy(enemy: 'sql.Enemy'):
return map_base_fields(models.Enemy, enemy)
def db_to_player(player: 'sql.Player'):
return map_base_fields(models.Player, player)
def db_to_penalty(penalty: 'sql.Penalty'):
model = map_base_fields(models.Penalty, penalty)
model.drink = db_to_drink(penalty.drink)
model.player = db_to_player(penalty.player)
return model
def db_to_death(death: 'sql.Death'):
model = map_base_fields(models.Death, death)
model.player = db_to_player(death.player)
model.enemy = db_to_enemy(death.enemy)
model.penalties = [db_to_penalty(penalty) for penalty in death.penalties]
return model
def db_to_victory(victory: 'sql.Victory'):
model = map_base_fields(models.Victory, victory)
model.player = db_to_player(victory.player)
model.enemy = db_to_enemy(victory.enemy)
return model
def db_to_episode(episode: 'sql.Episode'):
model = map_base_fields(models.Episode, episode)
model.players = [db_to_player(player) for player in episode.players]
model.deaths = [db_to_death(death) for death in episode.deaths]
model.victories = [db_to_victory(victory) for victory in episode.victories]
return model
def db_to_season(season: 'sql.Season'):
model = map_base_fields(models.Season, season)
model.enemies = [db_to_enemy(enemy) for enemy in season.enemies]
model.episodes = [db_to_episode(ep) for ep in season.episodes]
return model

View File

@@ -5,8 +5,14 @@ Example:
from sql import Episode
query = Episode.select().where(Episode.name == 'MyName')
"""
import sys
import datetime
from peewee import *
try:
from peewee import *
except ImportError:
print('peewee package not installed')
sys.exit(0)
db = MySQLDatabase(None)
@@ -56,7 +62,7 @@ class Drink(Model):
class Enemy(Model):
id = AutoField()
name = CharField()
optional = BooleanField()
boss = BooleanField()
season = ForeignKeyField(Season, backref='enemies')
class Meta:
@@ -66,6 +72,7 @@ class Enemy(Model):
class Death(Model):
id = AutoField()
info = CharField(null=True)
time = TimeField(default=datetime.time(0, 0))
player = ForeignKeyField(Player)
enemy = ForeignKeyField(Enemy)
episode = ForeignKeyField(Episode, backref='deaths')
@@ -88,6 +95,7 @@ class Penalty(Model):
class Victory(Model):
id = AutoField()
info = CharField(null=True)
time = TimeField(default=datetime.time(0, 0))
player = ForeignKeyField(Player)
enemy = ForeignKeyField(Enemy)
episode = ForeignKeyField(Episode, backref='victories')

View File

@@ -1,7 +1,7 @@
"""
This module contains shorthand functions for common queries to ease access from the UI
"""
from dsst_sql.sql import *
from dsst_server.data_access import sql
def get_episodes_for_season(season_id: int) -> list:
@@ -10,8 +10,8 @@ def get_episodes_for_season(season_id: int) -> list:
:return: List of sql.Episode or empty list
"""
try:
return list(Season.get(Season.id == season_id).episodes)
except Episode.DoesNotExist:
return list(sql.Season.get(sql.Season.id == season_id).episodes)
except sql.Episode.DoesNotExist:
return []
@@ -22,7 +22,7 @@ def get_player_deaths_for_season(season_id: int, player_id: int) -> int:
:return: Number of deaths of the player in the season
"""
deaths = 0
for episode in list(Season.get(Season.id == season_id).episodes):
for episode in list(sql.Season.get(sql.Season.id == season_id).episodes):
deaths = deaths + len([death for death in list(episode.deaths) if death.player.id == player_id])
return deaths
@@ -34,19 +34,33 @@ def get_player_victories_for_season(season_id: int, player_id: int) -> int:
:return: Number of all victories of the player in the season
"""
victories = 0
for episode in list(Season.get(Season.id == season_id).episodes):
for episode in list(sql.Season.get(sql.Season.id == season_id).episodes):
victories = victories + len([vic for vic in list(episode.victories) if vic.player.id == player_id])
return victories
def players_for_season(season_id: int) -> set:
season_eps = list(sql.Season.get(sql.Season.id == season_id).episodes)
players = set()
for ep in season_eps:
players.update([player for player in ep.players])
return players
def enemy_attempts(enemy_id: int) -> int:
return sql.Death.select().where(sql.Death.enemy == enemy_id).count()
def create_tables():
"""Create all database tables"""
models = [Season, Episode, Player, Drink, Enemy, Death, Victory, Penalty, Episode.players.get_through_model()]
models = [sql.Season, sql.Episode, sql.Player, sql.Drink, sql.Enemy, sql.Death, sql.Victory, sql.Penalty,
sql.Episode.players.get_through_model()]
for model in models:
model.create_table()
def drop_tables():
"""Drop all data in database"""
models = [Season, Episode, Player, Drink, Enemy, Death, Victory, Penalty, Episode.players.get_through_model()]
db.drop_tables(models)
models = [sql.Season, sql.Episode, sql.Player, sql.Drink, sql.Enemy, sql.Death, sql.Victory, sql.Penalty,
sql.Episode.players.get_through_model()]
sql.db.drop_tables(models)

View File

@@ -0,0 +1,6 @@
from dsst_server.func_write import WriteFunctions
from dsst_server.func_read import ReadFunctions
class FunctionProxy(WriteFunctions, ReadFunctions):
pass

View File

@@ -0,0 +1,52 @@
from dsst_server.data_access import sql, sql_func, mapping
from common import models
from playhouse import shortcuts
class ReadFunctions:
@staticmethod
def load_db_meta(*_):
return sql.db.database
@staticmethod
def load_seasons(*_):
return [mapping.db_to_season(season) for season in sql.Season.select()]
@staticmethod
def load_seasons_all(*_):
return [shortcuts.model_to_dict(season, backrefs=True, max_depth=2) for season in sql.Season.select()]
@staticmethod
def load_episodes(season_id, *_):
if not season_id:
raise Exception('Exception: Missing argument (season_id)')
return [mapping.db_to_episode(ep) for ep in sql.Season.get(sql.Season.id == season_id).episodes]
@staticmethod
def load_players(*_):
return [mapping.db_to_player(player) for player in sql.Player.select()]
@staticmethod
def load_enemies(season_id, *_):
pass
@staticmethod
def load_drinks(*_):
return [mapping.db_to_drink(drink) for drink in sql.Drink.select()]
@staticmethod
def load_season_stats(season_id, *_):
season = sql.Season.get(sql.Season.id == season_id)
players = sql_func.players_for_season(season_id)
model = models.SeasonStats()
model.player_kd = [(player.name,
sql_func.get_player_victories_for_season(season_id, player.id),
sql_func.get_player_deaths_for_season(season_id, player.id))
for player in players]
model.enemies = [(enemy.id,
enemy.name,
sql_func.enemy_attempts(enemy.id),
sql.Victory.select().where(sql.Victory.enemy == enemy.id).exists(),
enemy.boss)
for enemy in season.enemies]
return model

View File

@@ -0,0 +1,78 @@
from common import models
from dsst_server.data_access import sql
class WriteFunctions:
@staticmethod
def create_season(season: 'models.Season'):
return 'Season created.'
@staticmethod
def update_enemy(enemy: 'models.Enemy', *_):
(sql.Enemy
.insert(id=enemy.id, boss=enemy.boss, name=enemy.name, season=enemy.season)
.on_conflict(update={sql.Enemy.name: enemy.name,
sql.Enemy.boss: enemy.boss,
sql.Enemy.season: enemy.season})
.execute())
@staticmethod
def update_player(player: 'models.Player', *_):
(sql.Player
.insert(id=player.id, name=player.name, hex_id=player.hex_id)
.on_conflict(update={sql.Player.name: player.name,
sql.Player.hex_id: player.hex_id})
.execute())
@staticmethod
def update_drink(drink: 'models.Drink', *_):
(sql.Drink
.insert(id=drink.id, name=drink.name, vol=drink.vol)
.on_conflict(update={sql.Drink.name: drink.name,
sql.Drink.vol: drink.vol})
.execute())
@staticmethod
def save_death(death: 'models.Death'):
with sql.db.atomic():
created_id = (sql.Death
.insert(info=death.info, player=death.player, enemy=death.enemy, episode=death.episode,
time=death.time)
.execute())
for penalty in death.penalties:
sql.Penalty.create(death=created_id, size=penalty.size, drink=penalty.drink, player=penalty.player)
@staticmethod
def save_victory(victory: 'models.Victory'):
(sql.Victory
.insert(info=victory.info, player=victory.player, enemy=victory.enemy, time=victory.time,
episode=victory.episode, id=victory.id)
.execute())
@staticmethod
def update_season(season: 'models.Season', *_):
(sql.Season
.insert(id=season.id, number=season.number, game_name=season.game_name, start_date=season.start_date,
end_date=season.end_date)
.on_conflict(
update={sql.Season.number: season.number,
sql.Season.game_name: season.game_name,
sql.Season.start_date: season.start_date,
sql.Season.end_date: season.end_date})
.execute())
@staticmethod
def update_episode(episode: 'models.Episode', *_):
players = list(sql.Player.select().where(sql.Player.id << [player.id for player in episode.players]))
new_ep_id = (sql.Episode
.insert(id=episode.id, number=episode.number, seq_number=episode.number, name=episode.name,
date=episode.date, season=episode.season)
.on_conflict(update={sql.Episode.name: episode.name,
sql.Episode.seq_number: episode.seq_number,
sql.Episode.number: episode.number,
sql.Episode.date: episode.date,
sql.Episode.season: episode.season})
.execute())
db_episode = sql.Episode.get(sql.Episode.id == new_ep_id)
db_episode.players = players
db_episode.save()

View File

@@ -0,0 +1,99 @@
import json
import pickle
import socket
import sys
import os
from common import util, models
from dsst_server import func_read, func_write, tokens
from dsst_server.func_proxy import FunctionProxy
from dsst_server.data_access import sql, sql_func
PORT = 12345
HOST = socket.gethostname()
BUFFER_SIZE = 1024
class DsstServer:
def __init__(self, config={}):
self.socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print('Created socket')
self.socket_server.bind((HOST, PORT))
print('Bound socket to {} on host {}'.format(PORT, HOST))
# Initialize database
db_config = config.get('database')
sql.db.init(db_config.get('db_name'), user=db_config.get('user'), password=db_config.get('password'))
sql_func.create_tables()
print('Database initialized ({})'.format(sql.db.database))
# Load access tokens and map them to their allowed methods
read_actions = util.list_class_methods(func_read.ReadFunctions)
write_actions = util.list_class_methods(func_write.WriteFunctions)
parm_access = {
'r': read_actions,
'rw': read_actions + write_actions
}
self.tokens = {token: parm_access[perms] for token, perms in tokens.TOKENS}
print('Loaded auth tokens: {}'.format(self.tokens.keys()))
def run(self):
self.socket_server.listen(5)
print('Socket is listening')
while True:
client, address = self.socket_server.accept()
try:
print('Connection from {}'.format(address))
data = util.recv_msg(client)
request = pickle.loads(data)
print('Request: {}'.format(request))
# Validate auth token in request
token = request.get('auth_token')
if token not in self.tokens:
util.send_msg(client, pickle.dumps({'success': False, 'message': 'Auth token invalid'}))
print('Rejected request from {}. Auth token invalid ({})'.format(address, token))
continue
# Check read functions
action_name = request.get('action')
if action_name in self.tokens[token]:
action = getattr(FunctionProxy, action_name)
try:
value = action(*request.get('args'))
except Exception as e:
response = {'success': False, 'message': 'Exception was thrown on server.\n{}'.format(e)}
util.send_msg(client, pickle.dumps(response))
raise
response = {'success': True, 'data': value}
util.send_msg(client, pickle.dumps(response))
continue
else:
msg = 'Action does not exist on server ({})'.format(request.get('action'))
util.send_msg(client, pickle.dumps({'success': False, 'message': msg}))
except Exception as e:
print(e)
finally:
client.close()
print('Connection to client closed')
def load_config(config_path: str) -> dict:
with open(config_path) as config_file:
return json.load(config_file)
def main():
config = os.path.join(os.path.expanduser('~'), '.config', 'dsst', 'server.json')
server = DsstServer(load_config(config))
try:
server.run()
except KeyboardInterrupt:
print('Server stopped')
server.socket_server.close()
try:
sys.exit(0)
except SystemExit:
os._exit(0)

View File

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