master #44
2
.gitignore
vendored
|
|
@ -18,3 +18,5 @@ env/
|
|||
# 5. IDE / Editor Einstellungen
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
data_trans.py
|
||||
116
alembic.ini
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = migrations
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||
# for all available tokens
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory.
|
||||
prepend_sys_path = .
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the python>=3.9 or backports.zoneinfo library.
|
||||
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
|
||||
# string value is passed to ZoneInfo()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to migrations/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "version_path_separator" below.
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
|
||||
|
||||
# version path separator; As mentioned above, this is the character used to split
|
||||
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
||||
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
||||
# Valid values for version_path_separator are:
|
||||
#
|
||||
# version_path_separator = :
|
||||
# version_path_separator = ;
|
||||
# version_path_separator = space
|
||||
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
||||
|
||||
# set to 'true' to search source files recursively
|
||||
# in each "version_locations" directory
|
||||
# new in Alembic version 1.10
|
||||
# recursive_version_locations = false
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = mysql+pymysql://admin:PWM_556rtX@192.168.0.113:3306/league_db
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
||||
# hooks = ruff
|
||||
# ruff.type = exec
|
||||
# ruff.executable = %(here)s/.venv/bin/ruff
|
||||
# ruff.options = --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
|
|
@ -190,7 +190,7 @@ def get_player_statistic(player_id, system_id):
|
|||
SELECT
|
||||
sys.id AS gamesystem_id,
|
||||
sys.name AS gamesystem_name,
|
||||
sys.*,
|
||||
sys.description,
|
||||
stat.mmr,
|
||||
stat.games_in_system,
|
||||
stat.points,
|
||||
|
|
@ -206,6 +206,7 @@ def get_player_statistic(player_id, system_id):
|
|||
ON sys.id = stat.gamesystem_id AND stat.player_id = %s
|
||||
WHERE sys.id = %s
|
||||
"""
|
||||
|
||||
cursor.execute(query, (player_id, system_id))
|
||||
row = cursor.fetchone()
|
||||
|
||||
|
|
@ -214,6 +215,33 @@ def get_player_statistic(player_id, system_id):
|
|||
return row
|
||||
|
||||
|
||||
def get_player_rank(player_id, system_id):
|
||||
"""Holt den Rang eines Spielers in einem System basierend auf MMR (absteigend)."""
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
query = """
|
||||
SELECT COUNT(*) + 1 AS rank
|
||||
FROM player_game_statistic stat
|
||||
WHERE stat.gamesystem_id = %s
|
||||
AND stat.games_in_system > 0
|
||||
AND stat.mmr > (
|
||||
SELECT stat2.mmr
|
||||
FROM player_game_statistic stat2
|
||||
WHERE stat2.player_id = %s
|
||||
AND stat2.gamesystem_id = %s
|
||||
)
|
||||
"""
|
||||
cursor.execute(query, (system_id, player_id, system_id))
|
||||
row = cursor.fetchone()
|
||||
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
if row and row['rank'] is not None:
|
||||
return row['rank']
|
||||
return None
|
||||
|
||||
def join_league(player_id, gamesystem_id):
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor()
|
||||
|
|
@ -407,19 +435,21 @@ def get_gamesystems_data():
|
|||
return rows
|
||||
|
||||
|
||||
|
||||
def get_leaderboard(system_name):
|
||||
"""Holt alle Spieler eines Systems sortiert nach MMR für die Rangliste."""
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
query = """
|
||||
SELECT p.id, p.display_name, p.discord_name, stat.mmr
|
||||
FROM players p
|
||||
JOIN player_game_statistic stat ON p.id = stat.player_id
|
||||
JOIN gamesystems sys ON stat.gamesystem_id = sys.id
|
||||
WHERE sys.name = %s AND stat.games_in_system > 0
|
||||
ORDER BY stat.mmr DESC
|
||||
"""
|
||||
query = query = """
|
||||
SELECT p.id, p.display_name, p.discord_name, p.discord_id, p.discord_avatar_url,
|
||||
stat.mmr, stat.games_in_system, stat.wins, stat.loss, stat.avv_points
|
||||
FROM players p
|
||||
JOIN player_game_statistic stat ON p.id = stat.player_id
|
||||
JOIN gamesystems sys ON stat.gamesystem_id = sys.id
|
||||
WHERE sys.name = %s AND stat.games_in_system > 0
|
||||
ORDER BY stat.mmr DESC
|
||||
"""
|
||||
cursor.execute(query, (system_name,))
|
||||
rows = cursor.fetchall()
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ from nicegui import ui, app
|
|||
from gui import gui_style
|
||||
from data import data_api
|
||||
from gui.info_text import info_system
|
||||
from gui.templates.leaderboard import Leaderboard
|
||||
|
||||
def setup_routes():
|
||||
@ui.page('/statistic/{systemname}', dark=True)
|
||||
def gamesystem_statistic_page(systemname: str):
|
||||
@ui.page('/statistic/{system_name}', dark=True)
|
||||
def gamesystem_statistic_page(system_name: str):
|
||||
|
||||
if not app.storage.user.get('authenticated', False):
|
||||
ui.navigate.to('/')
|
||||
|
|
@ -14,112 +15,77 @@ def setup_routes():
|
|||
gui_style.apply_design()
|
||||
|
||||
player_id = app.storage.user.get('db_id')
|
||||
all_stats = data_api.get_player_statistics(player_id)
|
||||
system_id = data_api.get_gamesystem_id_by_name(system_name)
|
||||
print(player_id, system_id)
|
||||
|
||||
# Passendes System anhand des Namens (case-insensitive) herausfiltern
|
||||
system_stat = next(
|
||||
(s for s in all_stats if s["gamesystem_name"].lower() == systemname.lower()),
|
||||
None
|
||||
)
|
||||
player_stats = data_api.get_player_statistic(player_id, system_id)
|
||||
|
||||
# Header
|
||||
with ui.header().classes('items-center justify-between bg-zinc-900 p-4 shadow-lg'):
|
||||
ui.button(icon="arrow_back", on_click=lambda: ui.navigate.to('/')).props("round")
|
||||
ui.button("Spiel eintragen", on_click=lambda: ui.navigate.to(f'/add-match/{system_name}'))
|
||||
|
||||
if system_stat:
|
||||
mmr = system_stat["mmr"] or 0
|
||||
games = system_stat["games_in_system"] or 0
|
||||
points = system_stat["points"] or 0
|
||||
avv_points = system_stat.get("avv_points") or "-"
|
||||
last_played_raw = system_stat.get("last_played")
|
||||
last_played = str(last_played_raw)[:10] if last_played_raw else "-"
|
||||
with ui.column().classes('w-full items-center justify-center mt-10'):
|
||||
ui.label(f'Deine Statistik in {system_name}').classes('text-3xl justify-center font-bold text-normaltext')
|
||||
|
||||
with ui.header().classes('items-center justify-between bg-zinc-900 p-4 shadow-lg'):
|
||||
ui.button(icon="arrow_back", on_click=lambda: ui.navigate.to('/')).props("round")
|
||||
ui.button("Spiel eintragen", on_click=lambda: ui.navigate.to(f'/add-match/{systemname}'))
|
||||
# --- BLOCK 1 (MMR & Rang | Rangliste) ---
|
||||
with ui.element('div').classes("w-full grid grid-cols-1 lg:grid-cols-3 gap-4 mt-4"):
|
||||
with ui.column().classes("w-full gap-4"):
|
||||
with ui.card().classes("w-full items-center justify-center text-center"):
|
||||
with ui.row().classes("w-full items-center text-center"):
|
||||
ui.label("MMR Punkte: ").classes('justify-center text-2xl font-bold text-normaltext')
|
||||
ui.space()
|
||||
info_system.create_info_button("mmr_info")
|
||||
ui.label(str(player_stats["mmr"])).classes('text-4xl font-bold text-accent')
|
||||
|
||||
with ui.column().classes('w-full items-center justify-center mt-10'):
|
||||
ui.label(f'Deine Statistik in {systemname}').classes('text-3xl justify-center font-bold text-normaltext')
|
||||
with ui.card().classes("w-full items-center justify-center text-center"):
|
||||
with ui.row().classes("w-full items-center text-center"):
|
||||
ui.label("Rang: ").classes('justify-center text-2xl font-bold text-normaltext')
|
||||
ui.space()
|
||||
info_system.create_info_button("rang_info")
|
||||
ui.label(str(data_api.get_player_rank(player_id,system_id))).classes('text-4xl font-bold text-blue-100')
|
||||
|
||||
# --- BLOCK 1 (MMR & Rang | Rangliste) ---
|
||||
leaderboard_data = data_api.get_leaderboard(systemname)
|
||||
table_rows = []
|
||||
my_rank = "-"
|
||||
|
||||
with ui.card().classes("w-full lg:col-span-2 max-h-[50vh] overflow-y-auto"):
|
||||
ui.label("Liga Rangliste").classes("text-xl font-bold text-white mb-2 top-0 bg-zinc-900 z-10")
|
||||
Leaderboard(system_name)
|
||||
|
||||
for index, player in enumerate(leaderboard_data):
|
||||
current_rank = index + 1
|
||||
if player['id'] == player_id:
|
||||
my_rank = current_rank
|
||||
table_rows.append({
|
||||
'rank': current_rank,
|
||||
'trend': '➖',
|
||||
'name': f"{player['display_name']} 'aka' {player['discord_name']}",
|
||||
'mmr': player['mmr']
|
||||
})
|
||||
# --- BLOCK 2 (5 Karten) ---
|
||||
with ui.card().classes("w-full"):
|
||||
with ui.element('div').classes("w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"):
|
||||
|
||||
table_columns = [
|
||||
{'name': 'rank', 'label': '#', 'field': 'rank', 'align': 'left'},
|
||||
{'name': 'trend', 'label': 'Trend', 'field': 'trend', 'align': 'center'},
|
||||
{'name': 'name', 'label': 'Spieler', 'field': 'name', 'align': 'left'},
|
||||
{'name': 'mmr', 'label': 'MMR', 'field': 'mmr', 'align': 'left'},
|
||||
]
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Spiele: ").classes('text-2xl font-bold')
|
||||
ui.label(str(player_stats["games_in_system"])).classes('text-4xl font-bold text-blue-100')
|
||||
|
||||
with ui.element('div').classes("w-full grid grid-cols-1 lg:grid-cols-3 gap-4 mt-4"):
|
||||
with ui.column().classes("w-full gap-4"):
|
||||
with ui.card().classes("w-full items-center justify-center text-center"):
|
||||
with ui.row().classes("w-full items-center text-center"):
|
||||
ui.label("MMR Punkte: ").classes('justify-center text-2xl font-bold text-normaltext')
|
||||
ui.space()
|
||||
info_system.create_info_button("mmr_info")
|
||||
ui.label(str(mmr)).classes('text-4xl font-bold text-accent')
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Ø Punkte pro Spiel: ").classes('text-2xl font-bold')
|
||||
ui.label(str(player_stats["avv_points"])).classes('text-4xl font-bold text-blue-100')
|
||||
|
||||
with ui.card().classes("w-full items-center justify-center text-center"):
|
||||
with ui.row().classes("w-full items-center text-center"):
|
||||
ui.label("Rang: ").classes('justify-center text-2xl font-bold text-normaltext')
|
||||
ui.space()
|
||||
info_system.create_info_button("rang_info")
|
||||
ui.label(str(my_rank)).classes('text-4xl font-bold text-blue-100')
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Win-Rate: ").classes('text-2xl font-bold')
|
||||
ui.label("-").classes('text-4xl font-bold text-blue-100')
|
||||
|
||||
with ui.card().classes("w-full lg:col-span-2"):
|
||||
ui.label("Liga Rangliste").classes("text-xl font-bold text-white mb-2")
|
||||
ui.table(columns=table_columns, rows=table_rows, row_key='rank').classes('w-full bg-zinc-900 text-white')
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Letztes Spiel am: ").classes('text-2xl font-bold')
|
||||
ui.label(str(player_stats["last_played"])).classes('text-4xl font-bold text-blue-100')
|
||||
|
||||
# --- BLOCK 2 (5 Karten) ---
|
||||
with ui.card().classes("w-full"):
|
||||
with ui.element('div').classes("w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"):
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Win-Streak: ").classes('text-2xl font-bold')
|
||||
ui.label("-").classes('text-4xl font-bold text-blue-100')
|
||||
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Spiele: ").classes('text-2xl font-bold')
|
||||
ui.label(str(games)).classes('text-4xl font-bold text-blue-100')
|
||||
# --- BLOCK 3 (3 Karten) ---
|
||||
with ui.card().classes("w-full"):
|
||||
with ui.element('div').classes("w-full grid grid-cols-1 md:grid-cols-3 lg:grid-cols-3 gap-4"):
|
||||
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Ø Punkte pro Spiel: ").classes('text-2xl font-bold')
|
||||
ui.label(str(avv_points)).classes('text-4xl font-bold text-blue-100')
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Dein Nemesis: ").classes('text-2xl font-bold')
|
||||
ui.label(str(player_stats["nemesis_id"])).classes('text-4xl font-bold text-blue-100')
|
||||
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Win-Rate: ").classes('text-2xl font-bold')
|
||||
ui.label("-").classes('text-4xl font-bold text-blue-100')
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Meisten Spiele mit: ").classes('text-2xl font-bold')
|
||||
ui.label(str(player_stats["nemesis_id"])).classes('text-4xl font-bold text-blue-100')
|
||||
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Letztes Spiel am: ").classes('text-2xl font-bold')
|
||||
ui.label(last_played).classes('text-4xl font-bold text-blue-100')
|
||||
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Win-Streak: ").classes('text-2xl font-bold')
|
||||
ui.label("-").classes('text-4xl font-bold text-blue-100')
|
||||
|
||||
# --- BLOCK 3 (3 Karten) ---
|
||||
with ui.card().classes("w-full"):
|
||||
with ui.element('div').classes("w-full grid grid-cols-1 md:grid-cols-3 lg:grid-cols-3 gap-4"):
|
||||
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Dein Nemesis: ").classes('text-2xl font-bold')
|
||||
ui.label("-").classes('text-4xl font-bold text-blue-100')
|
||||
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Meisten Spiele mit: ").classes('text-2xl font-bold')
|
||||
ui.label("-").classes('text-4xl font-bold text-blue-100')
|
||||
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Dein 'Prügelknabe': ").classes('text-2xl font-bold')
|
||||
ui.label("-").classes('text-4xl font-bold text-blue-100')
|
||||
|
||||
else:
|
||||
with ui.column().classes('w-full items-center justify-center mt-10'):
|
||||
ui.label(f'Keine Statistik für "{systemname}" gefunden.').classes('text-red-500 text-xl mt-4')
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Dein 'Prügelknabe': ").classes('text-2xl font-bold')
|
||||
ui.label(str(player_stats["pushover_id"])).classes('text-4xl font-bold text-blue-100')
|
||||
24
gui/league_statistic_visitor_gui.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from nicegui import ui, app
|
||||
from gui import gui_style
|
||||
from data import data_api
|
||||
from gui.info_text import info_system
|
||||
|
||||
from gui.templates.leaderboard import Leaderboard
|
||||
|
||||
|
||||
|
||||
def setup_routes():
|
||||
@ui.page('/statistic/visitor/{systemname}', dark=True)
|
||||
def gamesystem_statistic_visitor_page(systemname: str):
|
||||
if not app.storage.user.get('authenticated', False):
|
||||
ui.navigate.to('/')
|
||||
return
|
||||
|
||||
gui_style.apply_design()
|
||||
|
||||
with ui.header().classes('items-center justify-between bg-zinc-900 p-4 shadow-lg'):
|
||||
ui.button(icon="arrow_back", on_click=lambda: ui.navigate.to('/')).props("round")
|
||||
|
||||
with ui.column().classes('w-full items-center justify-center mt-10'):
|
||||
ui.label(f'Die aktuelle Statistik in {systemname}').classes('text-3xl justify-center font-bold text-normaltext')
|
||||
Leaderboard(systemname)
|
||||
|
|
@ -21,7 +21,9 @@ def setup_routes(admin_discord_id):
|
|||
|
||||
ui.navigate.reload()
|
||||
return
|
||||
# -----------------------------------------
|
||||
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# --- NAVIGATIONSLEISTE (HEADER)
|
||||
# ---------------------------
|
||||
|
|
@ -31,19 +33,19 @@ def setup_routes(admin_discord_id):
|
|||
# --- LINKE SEITE ---
|
||||
# Vereinslogo und den Titel in einer eigenen Reihe (Reihe 1)
|
||||
with ui.row().classes('items-center'):
|
||||
ui.image("gui/pictures/wsdg.png").classes('w-15 h-15 rounded-full')
|
||||
ui.label('Diceghost Liga').classes('text-2xl font-bold text-normaltext')
|
||||
ui.image("gui/pictures/wsdg.png").classes('w-12 h-12 md:w-25 md:h-25 rounded-full')
|
||||
ui.label('Westside Diceghost Liga').classes('text-lg md:text-3xl font-bold text-normaltext')
|
||||
|
||||
# --- MITTE ---
|
||||
if app.storage.user.get('authenticated', False):
|
||||
discord_id = app.storage.user.get("discord_id")
|
||||
if discord_id == admin_discord_id:
|
||||
ui.button(icon="hardware", on_click=lambda: ui.navigate.to('/admin')).props("round")
|
||||
ui.button(icon="hardware", on_click=lambda: ui.navigate.to('/admin')).classes('w-2 h-2')
|
||||
|
||||
# --- RECHTE SEITE ---
|
||||
if app.storage.user.get('authenticated', False):
|
||||
with ui.row().classes('items-center gap-4'):
|
||||
ui.image(app.storage.user.get('discord_avatar_url')).classes('w-15 h-15 rounded-full')
|
||||
ui.image(app.storage.user.get('discord_avatar_url')).classes('w-10 h-10 md:w-25 md:h-25 rounded-full border-1 border-red-900')
|
||||
discord_name = app.storage.user.get('discord_name')
|
||||
display_name = app.storage.user.get('display_name')
|
||||
player_id = app.storage.user.get('db_id')
|
||||
|
|
@ -57,10 +59,10 @@ def setup_routes(admin_discord_id):
|
|||
# --- ANSICHT 1: Der normale Text mit Edit-Button ---
|
||||
with ui.column().classes('items-center gap-0') as display_row:
|
||||
with ui.column():
|
||||
ui.label(display_name).classes('text-xl font-bold text-normaltext')
|
||||
ui.label(display_name).classes('md:text-3xl font-bold text-normaltext')
|
||||
with ui.row().classes("items-center justify-between"):
|
||||
ui.label("'aka'").classes('text-sm text-italic text-infotext')
|
||||
ui.label(discord_name).classes('text-m text-bold text-infotext')
|
||||
ui.label("'aka'").classes('md:text-lg text-xs text-italic text-infotext')
|
||||
ui.label(discord_name).classes('text-sm md:text-xl text-bold text-infotext')
|
||||
edit_button = ui.button(icon='edit', color='accent', on_click=toggle_edit_mode).props('round dense')
|
||||
|
||||
# --- ANSICHT 2: Das Eingabefeld (startet unsichtbar!) ---
|
||||
|
|
@ -201,19 +203,25 @@ def setup_routes(admin_discord_id):
|
|||
sys_description = sys['description']
|
||||
|
||||
sys_card = ui.card().classes("h-60 w-full items-center justify-center transition-colors")
|
||||
|
||||
with sys_card:
|
||||
ui.label(text=sys_name).classes('text-xl font-bold text-center text-normaltext')
|
||||
if sys_logo:
|
||||
ui.image(f"/pictures/{sys_logo}").classes("w-60")
|
||||
|
||||
if sys_description:
|
||||
ui.label(text=sys['description']).classes('text-xs text-gray-400 text-center mt-2 text-infotext')
|
||||
with sys_card:
|
||||
# Wir erstellen einen Container für den "klickbaren" Teil
|
||||
clickable_content = ui.column().classes("w-full h-full items-center")
|
||||
|
||||
with clickable_content:
|
||||
ui.label(text=sys_name).classes('text-xl font-bold text-center text-normaltext')
|
||||
if sys_logo:
|
||||
ui.image(f"/pictures/{sys_logo}").classes("w-60")
|
||||
if sys_description:
|
||||
ui.label(text=sys['description']).classes('text-xs text-gray-400 text-center mt-2 text-infotext')
|
||||
|
||||
# Prüfen: Ist diese sys_id in den Stats des Spielers? UND hat er ein MMR?
|
||||
if sys_id not in my_stats or my_stats[sys_id]['mmr'] is None:
|
||||
ui.label(text="Du bist noch nicht in dieser Liga.").classes("text-red-500 font-bold")
|
||||
|
||||
clickable_content.classes("cursor-pointer hover:bg-zinc-800")
|
||||
clickable_content.on('click', lambda e, name=sys_name: ui.navigate.to(f'/statistic/visitor/{name}'))
|
||||
|
||||
join_row = ui.row().classes('items-center gap-2')
|
||||
confirm_row = ui.row().classes('items-center gap-2')
|
||||
confirm_row.visible = False
|
||||
|
|
@ -225,11 +233,12 @@ def setup_routes(admin_discord_id):
|
|||
ui.button("Liga Beitreten", color="green", on_click=lambda e, p=player_id, s=sys_id: click_join_league(p, s))
|
||||
ui.button(icon='cancel', color='red', on_click=lambda e, r1=join_row, r2=confirm_row: toggle_visibility(r1, r2)).props('round dense')
|
||||
else:
|
||||
# Spieler IST in der Liga!
|
||||
stat = my_stats[sys_id] # Wir holen uns seine Stats aus dem Wörterbuch
|
||||
# Spieler IST in der Liga
|
||||
stat = my_stats[sys_id]
|
||||
|
||||
sys_card.classes("cursor-pointer hover:bg-zinc-800")
|
||||
sys_card.on('click', lambda e, name=sys_name: ui.navigate.to(f'/statistic/{name}'))
|
||||
# JETZT machen wir nur den Container klickbar, nicht die ganze Karte!
|
||||
clickable_content.classes("cursor-pointer hover:bg-zinc-800")
|
||||
clickable_content.on('click', lambda e, name=sys_name: ui.navigate.to(f'/statistic/{name}'))
|
||||
|
||||
with ui.row().classes('items-center gap-4'):
|
||||
ui.label(text=f"MMR: {stat['mmr']}").classes("text-lg font-bold text-accenttext")
|
||||
|
|
|
|||
|
|
@ -33,19 +33,12 @@ def setup_routes():
|
|||
raw_players = data_api.get_all_players_from_system(system_name)
|
||||
my_id = app.storage.user.get('db_id')
|
||||
|
||||
def add_point():
|
||||
p1_points.value += 1
|
||||
|
||||
def sub_point():
|
||||
p1_points.value -= 1
|
||||
|
||||
with ui.row().classes("w-full items-center justify-between"):
|
||||
p1_points = ui.slider(min=min_score, max=max_score, value=10).classes("w-35")
|
||||
with ui.column().classes("items-center justify-between"):
|
||||
# Punkte Up- Down- Buttons. und Textanzeige
|
||||
ui.button(icon="expand_less", on_click=add_point)
|
||||
ui.label().bind_text_from(p1_points, 'value').classes("text-lg text-normaltext")
|
||||
ui.button(icon="expand_more", on_click=sub_point)
|
||||
with ui.column().classes("items-center justify-between"):
|
||||
p1_points = ui.number(placeholder="Punkte eingeben",
|
||||
min=system_data["min_score"],
|
||||
max=system_data["max_score"],
|
||||
precision=0
|
||||
).classes("w-35").props("clearable")
|
||||
|
||||
with ui.card().classes('w-full max-w-md mx-auto items-center mt-10 p-6'):
|
||||
dropdown_options = {}
|
||||
|
|
@ -56,36 +49,39 @@ def setup_routes():
|
|||
|
||||
opponent_select = ui.select(options=dropdown_options, label='Gegner auswählen').classes('w-full')
|
||||
|
||||
def add_point():
|
||||
p2_points.value += 1
|
||||
|
||||
def sub_point():
|
||||
p2_points.value -= 1
|
||||
|
||||
with ui.row().classes("w-full items-center justify-between"):
|
||||
p2_points = ui.slider(min=min_score, max=max_score, value=10).classes("w-35")
|
||||
with ui.column().classes("items-center justify-between"):
|
||||
# Punkte Up- Down- Buttons. und Textanzeige
|
||||
ui.button(icon="expand_less", on_click=add_point)
|
||||
ui.label().bind_text_from(p2_points, 'value').classes("text-lg text-normaltext")
|
||||
ui.button(icon="expand_more", on_click=sub_point)
|
||||
with ui.column().classes("items-center justify-between"):
|
||||
p2_points = ui.number(placeholder="Punkte eingeben",
|
||||
min=system_data["min_score"],
|
||||
max=system_data["max_score"],
|
||||
precision=0
|
||||
).classes("w-35").props("clearable")
|
||||
|
||||
|
||||
|
||||
# Das Match in die Datenbank eintragen lassen und die MMR Berechnung triggern.
|
||||
# Das Match in die Datenbank eintragen lassen.
|
||||
def input_match_to_database():
|
||||
p2_id = opponent_select.value
|
||||
score_p1 = p1_points.value
|
||||
score_p2 = p2_points.value
|
||||
|
||||
|
||||
# Check der Eingegebenen Daten.
|
||||
# Fehler wenn kein Spieler oder keine Punkte eingegeben werden. Das Punkte Eingabefeld fängt mit min_score und max_score ab,
|
||||
# ob für das Spielsystem legale Punkte eingetragen wurden.
|
||||
if p2_id is None:
|
||||
ui.notify("Bitte wähle zuerst einen Gegner aus!", color="red", position="top")
|
||||
return
|
||||
|
||||
score_p1 = p1_points.value
|
||||
score_p2 = p2_points.value
|
||||
if score_p1 is None:
|
||||
ui.notify("Ungültige Punkteeingabe Spieler 1!", color="red", position="top")
|
||||
return
|
||||
|
||||
if score_p2 is None:
|
||||
ui.notify("Ungültige Punkteeingabe Spieler 2!", color="red", position="top")
|
||||
return
|
||||
|
||||
match_id = data_api.add_new_match(system_name, my_id, p2_id, score_p1, score_p2)
|
||||
|
||||
# 4. Erfolgsmeldung und Berechnung
|
||||
ui.notify("Match erfolgreich eingetragen!", color="green")
|
||||
ui.navigate.to(f'/statistic/{system_name}')
|
||||
|
||||
# Buttons ganz unten in einer Reihe
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
BIN
gui/pictures/achievments/AchievementIcon_4_1Sieg.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
gui/pictures/achievments/AchievementIcon_5_5Sieg.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
gui/pictures/achievments/AchievementIcon_6_10Sieg.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
24
gui/templates/leaderboard.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from nicegui import ui
|
||||
from data import data_api
|
||||
from gui.templates.player_card import PlayerCard
|
||||
|
||||
class Leaderboard:
|
||||
def __init__(self, system_name: str):
|
||||
# Daten holen
|
||||
leaderboard_data = data_api.get_leaderboard(system_name)
|
||||
|
||||
# Container
|
||||
with ui.column().classes('w-full gap-2') as self.container:
|
||||
for index, player in enumerate(leaderboard_data):
|
||||
PlayerCard(
|
||||
rank=index + 1,
|
||||
display_name=player['display_name'],
|
||||
discord_name=player['discord_name'],
|
||||
mmr=player['mmr'],
|
||||
discord_avatar_url=player.get('discord_avatar_url'),
|
||||
discord_id=player.get('discord_id'),
|
||||
games_in_system=player.get('games_in_system', 0),
|
||||
wins=player.get('wins', 0),
|
||||
loss=player.get('loss', 0),
|
||||
avv_points=player.get('avv_points', 0)
|
||||
)
|
||||
41
gui/templates/player_card.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
from nicegui import ui
|
||||
|
||||
class PlayerCard:
|
||||
def __init__(self, rank: int, display_name: str, discord_name: str, mmr: int,
|
||||
discord_avatar_url: str = None, discord_id: str = None,
|
||||
games_in_system: int = 0, wins: int = 0, loss: int = 0, avv_points: int = 0):
|
||||
|
||||
# Rang links, Card rechts
|
||||
with ui.row().classes('w-full items-center gap-1 flex-nowrap'):
|
||||
ui.label(f'#{rank}').classes('text-2xl font-bold w-10 text-center shrink-0')
|
||||
|
||||
# Player Card
|
||||
with ui.card().classes("flex-1 no-shadow bg-zinc-900 min-w-0 overflow-hidden"):
|
||||
# Discord Block - klickbar
|
||||
if discord_id:
|
||||
with ui.link(target=f'https://discord.com/users/{discord_id}', new_tab=True).classes('no-underline w-full'):
|
||||
self._discord_content(discord_avatar_url, display_name, discord_name)
|
||||
else:
|
||||
self._discord_content(discord_avatar_url, display_name, discord_name)
|
||||
|
||||
# Stats und Achievments
|
||||
with ui.row().classes('w-full items-center flex-nowrap'):
|
||||
with ui.row().classes('flex-2 items-center gap-2 md:gap-3 md:justify-center shrink min-w-0'):
|
||||
ui.label(f'{mmr} MMR').classes('font-bold md:text-lg')
|
||||
with ui.row().classes('flex-1 justify-end items-center gap-1 md:gap-3 shrink-0'):
|
||||
ui.image('gui/pictures/achievments/AchievementIcon_4_1Sieg.png').classes('w-5 md:w-15')
|
||||
ui.image('gui/pictures/achievments/AchievementIcon_4_1Sieg.png').classes('w-5 md:w-15')
|
||||
ui.image('gui/pictures/achievments/AchievementIcon_4_1Sieg.png').classes('w-5 md:w-15')
|
||||
|
||||
|
||||
def _discord_content(self, avatar_url, display_name, discord_name):
|
||||
short_name = display_name[:15] + '...' if len(display_name) > 15 else display_name
|
||||
short_discord = discord_name[:15] + '...' if len(discord_name) > 15 else discord_name
|
||||
|
||||
with ui.card().classes("w-full md:w-90 bg-indigo-950 no-shadow"):
|
||||
with ui.row().classes('items-center gap-3 min-w-0'):
|
||||
if avatar_url:
|
||||
ui.image(avatar_url).classes('w-10 h-10 md:w-20 md:h-20 rounded-full border-red-900 border-2 shrink-0')
|
||||
with ui.column().classes('gap-0 min-w-0'):
|
||||
ui.label(short_name).classes('text-sm md:text-2xl font-bold whitespace-nowrap truncate')
|
||||
ui.label(short_discord).classes('text-xs md:text-sm opacity-60 whitespace-nowrap truncate')
|
||||
18
main.py
|
|
@ -1,11 +1,13 @@
|
|||
import os
|
||||
import schedule
|
||||
from dotenv import load_dotenv
|
||||
from nicegui import ui, app
|
||||
|
||||
from data import database
|
||||
from gui import main_gui, match_gui, discord_login, league_statistic_gui, admin_gui, match_history_gui, imprint_gui
|
||||
from gui import main_gui, match_gui, discord_login, league_statistic_gui, admin_gui, match_history_gui, imprint_gui, league_statistic_visitor_gui
|
||||
from wood import logger
|
||||
from gui.info_text import info_system
|
||||
from utils import season_scheduler
|
||||
|
||||
# 1. Lade die geheimen Variablen aus der .env Datei in den Speicher
|
||||
load_dotenv()
|
||||
|
|
@ -33,6 +35,18 @@ match_gui.setup_routes()
|
|||
admin_gui.setup_routes()
|
||||
match_history_gui.setup_routes()
|
||||
imprint_gui.setup_routes()
|
||||
league_statistic_visitor_gui.setup_routes()
|
||||
|
||||
# 4. Wir starten die NiceGUI App
|
||||
# Timer der schedule regelmäßig prüft
|
||||
@app.on_startup
|
||||
def setup_scheduler():
|
||||
import threading
|
||||
from utils.season_scheduler import run_scheduler
|
||||
|
||||
thread = threading.Thread(target=run_scheduler, daemon=True)
|
||||
thread.start()
|
||||
|
||||
|
||||
|
||||
# 4. Starten der App Runtime auf dem Server.
|
||||
ui.run(title="Westside Diceghost Liga", port=9000, storage_secret="EIN_super-geheimes_Pa$$wort#!", favicon="gui/pictures/wsdg.png")
|
||||
|
|
|
|||
1
migrations/README
Normal file
|
|
@ -0,0 +1 @@
|
|||
Generic single-database configuration.
|
||||
78
migrations/env.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
target_metadata = None
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection, target_metadata=target_metadata
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
16
migrations/generate_migration_code.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from sqlalchemy import create_engine, MetaData
|
||||
|
||||
DATABASE_URL = "mysql+pymysql://admin:PWM_556rtX@192.168.0.113:3306/league_db"
|
||||
engine = create_engine(DATABASE_URL)
|
||||
metadata = MetaData()
|
||||
metadata.reflect(bind=engine)
|
||||
|
||||
print("Kopiere das hier in dein Alembic-Script:")
|
||||
for table in metadata.tables.values():
|
||||
print(f"\n# Tabellen-Definition für: {table.name}")
|
||||
print(f"op.create_table('{table.name}',")
|
||||
for column in table.columns:
|
||||
col_type = str(column.type)
|
||||
nullable = "True" if column.nullable else "False"
|
||||
print(f" sa.Column('{column.name}', sa.{col_type.split('(')[0].upper()}(), nullable={nullable}),")
|
||||
print(")")
|
||||
28
migrations/script.py.mako
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
${downgrades if downgrades else "pass"}
|
||||
129
migrations/versions/47a1e41aa62f_initial_setup.py
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
"""initial_setup
|
||||
|
||||
Revision ID: 47a1e41aa62f
|
||||
Revises:
|
||||
Create Date: 2026-05-28 08:32:26.064485
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '47a1e41aa62f'
|
||||
down_revision: Union[str, Sequence[str], None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
# Tabellen-Definition für: achievements
|
||||
op.create_table('achievements',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('name', sa.VARCHAR(), nullable=False),
|
||||
sa.Column('description', sa.TEXT(), nullable=False),
|
||||
sa.Column('icon', sa.TEXT(), nullable=True),
|
||||
sa.Column('misc', sa.TEXT(), nullable=True),
|
||||
)
|
||||
|
||||
# Tabellen-Definition für: gamesystems
|
||||
op.create_table('gamesystems',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('name', sa.VARCHAR(), nullable=False),
|
||||
sa.Column('picture', sa.TEXT(), nullable=True),
|
||||
sa.Column('description', sa.TEXT(), nullable=True),
|
||||
sa.Column('active_players', sa.INTEGER(), nullable=True),
|
||||
sa.Column('min_score', sa.INTEGER(), nullable=True),
|
||||
sa.Column('max_score', sa.INTEGER(), nullable=True),
|
||||
)
|
||||
|
||||
# Tabellen-Definition für: matches
|
||||
op.create_table('matches',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('gamesystem_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('player1_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('score_player1', sa.INTEGER(), nullable=True),
|
||||
sa.Column('player2_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('score_player2', sa.INTEGER(), nullable=True),
|
||||
sa.Column('played_at', sa.TIMESTAMP(), nullable=True),
|
||||
sa.Column('player1_base_change', sa.INTEGER(), nullable=True),
|
||||
sa.Column('player1_khorne', sa.INTEGER(), nullable=True),
|
||||
sa.Column('player1_slaanesh', sa.INTEGER(), nullable=True),
|
||||
sa.Column('player1_tzeentch', sa.INTEGER(), nullable=True),
|
||||
sa.Column('player1_mmr_change', sa.INTEGER(), nullable=True),
|
||||
sa.Column('player2_base_change', sa.INTEGER(), nullable=True),
|
||||
sa.Column('player2_khorne', sa.INTEGER(), nullable=True),
|
||||
sa.Column('player2_slaanesh', sa.INTEGER(), nullable=True),
|
||||
sa.Column('player2_tzeentch', sa.INTEGER(), nullable=True),
|
||||
sa.Column('player2_mmr_change', sa.INTEGER(), nullable=True),
|
||||
sa.Column('player2_check', sa.INTEGER(), nullable=True),
|
||||
sa.Column('match_is_counted', sa.INTEGER(), nullable=True),
|
||||
sa.Column('rust_factor', sa.DOUBLE(), nullable=True),
|
||||
sa.Column('point_inflation', sa.DOUBLE(), nullable=True),
|
||||
sa.Column('elo_factor', sa.DOUBLE(), nullable=True),
|
||||
)
|
||||
|
||||
# Tabellen-Definition für: players
|
||||
op.create_table('players',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('discord_id', sa.VARCHAR(), nullable=True),
|
||||
sa.Column('discord_name', sa.VARCHAR(), nullable=False),
|
||||
sa.Column('discord_avatar_url', sa.TEXT(), nullable=True),
|
||||
sa.Column('display_name', sa.VARCHAR(), nullable=True),
|
||||
)
|
||||
|
||||
# Tabellen-Definition für: player_achievements
|
||||
op.create_table('player_achievements',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('player_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('achievement_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('earned_at', sa.TIMESTAMP(), nullable=True),
|
||||
sa.Column('earned_in', sa.INTEGER(), nullable=True),
|
||||
)
|
||||
|
||||
# Tabellen-Definition für: player_game_statistic
|
||||
op.create_table('player_game_statistic',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('player_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('gamesystem_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('mmr', sa.INTEGER(), nullable=True),
|
||||
sa.Column('games_in_system', sa.INTEGER(), nullable=True),
|
||||
sa.Column('points', sa.INTEGER(), nullable=True),
|
||||
sa.Column('avv_points', sa.INTEGER(), nullable=True),
|
||||
sa.Column('last_played', sa.TIMESTAMP(), nullable=True),
|
||||
sa.Column('win_rate', sa.INTEGER(), nullable=True),
|
||||
sa.Column('win_streak', sa.INTEGER(), nullable=True),
|
||||
sa.Column('wins', sa.INTEGER(), nullable=True),
|
||||
sa.Column('loss', sa.INTEGER(), nullable=True),
|
||||
sa.Column('draws', sa.INTEGER(), nullable=True),
|
||||
sa.Column('trend', sa.INTEGER(), nullable=True),
|
||||
sa.Column('tyrann_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('pushover_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('nemesis_id', sa.INTEGER(), nullable=True),
|
||||
sa.Column('season_id', sa.INTEGER(), nullable=False),
|
||||
)
|
||||
|
||||
# Tabellen-Definition für: seasons
|
||||
op.create_table('seasons',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('gamesystem_id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('name', sa.VARCHAR(), nullable=False),
|
||||
sa.Column('start_date', sa.DATE(), nullable=False),
|
||||
sa.Column('end_date', sa.DATE(), nullable=True),
|
||||
)
|
||||
|
||||
# Tabellen-Definition für: season_schedule
|
||||
op.create_table('season_schedule',
|
||||
sa.Column('id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('gamesystem_id', sa.INTEGER(), nullable=False),
|
||||
sa.Column('start_month', sa.INTEGER(), nullable=False),
|
||||
sa.Column('start_day', sa.INTEGER(), nullable=False),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
pass
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
nicegui
|
||||
requests
|
||||
python-dotenv
|
||||
mariadb
|
||||
89
setup_env.sh
|
|
@ -1,15 +1,92 @@
|
|||
#!/bin/bash
|
||||
|
||||
# 1. Erstellt die virtuelle Umgebung im Ordner ".venv" (falls sie nicht existiert)
|
||||
# Sicherstellen dass das Script abbricht wenn ein Fehler passiert
|
||||
# -e: Beendet das Script sofort wenn ein Befehl einen Fehlercode zurückgibt (Exit-Code != 0)
|
||||
set -e
|
||||
|
||||
echo "✅ Starte einrichtung der Liga Umgebung. Updates und System Abhängigkeiten werden installiert ..."
|
||||
|
||||
# Prüfen ob der Liga-Service auf dem System installiert ist
|
||||
# systemctl list-unit-files zeigt alle registrierten Service-Dateien
|
||||
SERVICE_EXISTS=false
|
||||
if systemctl list-unit-files --type service | grep -q "liga.service"; then
|
||||
# Service existiert → sicher anhalten bevor Updates kommen
|
||||
sudo systemctl stop liga
|
||||
SERVICE_EXISTS=true
|
||||
echo "✅ Liga Service wurde gestoppt."
|
||||
else
|
||||
# Service nicht installiert → kein Fehler, nur Info
|
||||
echo "ℹ️ Liga Service nicht gefunden. Wird übersprungen."
|
||||
fi
|
||||
|
||||
# 1. System updaten
|
||||
# apt update: Lädt die aktuellen Paketlisten vom Server herunter
|
||||
# apt upgrade -y: Installiert alle verfügbaren Updates, -y bestätigt automatisch
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
|
||||
# 2. System-Abhängigkeiten installieren
|
||||
# -y: Automatisch mit "Ja" bestätigen
|
||||
sudo apt install -y \
|
||||
python3 \
|
||||
python3-venv \
|
||||
python3-full \
|
||||
libmariadb3 \
|
||||
libmariadb-dev \
|
||||
alembic
|
||||
|
||||
echo "✅ Updates installiert. Überprüfe Softwarestand von Git ..."
|
||||
|
||||
# Git Pull: Holt die neuesten Änderungen vom Remote-Repository
|
||||
# und merged sie in den lokalen Branch
|
||||
git pull
|
||||
|
||||
echo "✅ Softwarestand von Git aktuell. Richte Umgebung ein ..."
|
||||
|
||||
# 3. Virtuelle Umgebung erstellen (falls noch nicht vorhanden)
|
||||
# Wenn .venv schon existiert wird es einfach aktualisiert/überschrieben
|
||||
python3 -m venv .venv
|
||||
|
||||
# 2. Aktiviert die virtuelle Umgebung für dieses Skript
|
||||
# 4. Virtuelle Umgebung aktivieren
|
||||
# WICHTIG: Da wir in einem Bash-Script sind, gilt die Aktivierung nur für dieses Script
|
||||
source .venv/bin/activate
|
||||
|
||||
# 3. Aktualisiert das Installationsprogramm "pip" auf die neueste Version
|
||||
echo "✅ Umgebung eingerichtet! Installiere Pakete ..."
|
||||
|
||||
# 5. pip aktualisieren
|
||||
# pip ist der Paketmanager für Python – sollte aktuell sein um Kompatibilitätsprobleme zu vermeiden
|
||||
pip install --upgrade pip
|
||||
|
||||
# 4. Installiert alle Pakete aus deiner "Stückliste"
|
||||
pip install -r requirements.txt
|
||||
# 6. Python-Pakete installieren
|
||||
pip install \
|
||||
nicegui \
|
||||
requests \
|
||||
python-dotenv \
|
||||
pymysql \
|
||||
schedule \
|
||||
sqlalchemy \
|
||||
alembic
|
||||
|
||||
echo "✅ Pakete installiert. Starte Liga Service ..."
|
||||
|
||||
# Prüfen ob der Service vorher existierte und versuche ihn zu starten
|
||||
if [ "$SERVICE_EXISTS" = true ]; then
|
||||
# Service existiert bereits auf dem System → versuchen zu starten
|
||||
# systemctl start gibt Exit-Code 0 bei Erfolg, != 0 bei Fehler
|
||||
if sudo systemctl start liga; then
|
||||
echo "✅ Liga Service erfolgreich gestartet. Alles bereit!"
|
||||
else
|
||||
# Service konnte nicht gestartet werden – mögliche Ursachen:
|
||||
# - Konfigurationsfehler in der .service Datei
|
||||
# - Python-Fehler beim Start (Import-Error, Syntax-Error etc.)
|
||||
# - Port bereits belegt
|
||||
# - Fehlende Berechtigungen
|
||||
echo "❌ FEHLER: Liga Service konnte nicht gestartet werden!"
|
||||
echo " Prüfe mit: sudo systemctl status liga"
|
||||
echo " Prüfe mit: sudo journalctl -u liga --no-pager -n 50"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Service war vorher auch nicht installiert – Info an den User
|
||||
echo "ℹ️ Liga Service nicht installiert. Manuell starten oder Service erst einrichten."
|
||||
fi
|
||||
|
||||
echo "Umgebung wurde erfolgreich eingerichtet!"
|
||||
|
|
|
|||
31
utils/season_scheduler.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import schedule
|
||||
import time
|
||||
from datetime import date
|
||||
from data import database
|
||||
|
||||
|
||||
def check_new_seasons():
|
||||
today = date.today()
|
||||
|
||||
conn = database.db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute(
|
||||
"SELECT id, gamesystem_id FROM season_schedule "
|
||||
"WHERE start_month = %s AND start_day = %s",
|
||||
(today.month, today.day)
|
||||
)
|
||||
triggers = cursor.fetchall()
|
||||
|
||||
print(f"[Season Check] {today} – {len(triggers)} Treffer gefunden")
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
def run_scheduler():
|
||||
"""Läuft in einem Hintergrund-Thread."""
|
||||
schedule.every().day.at("00:01").do(check_new_seasons)
|
||||
|
||||
while True:
|
||||
schedule.run_pending()
|
||||
time.sleep(60)
|
||||