Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d5cd05c0e5 | |||
| b8c1df5ff8 | |||
| 8877de2cef | |||
| bd5c142bcf | |||
| e0ed3c7059 | |||
| a53e7100da | |||
| 2902c6a58c | |||
| 4541d3763f | |||
| ce45c389ef | |||
| edf1d70b54 | |||
| 8b02390bee | |||
| b47eefe615 | |||
| 57c578a44b | |||
| d57f4baedd | |||
| f4db57b2ff | |||
| d83182dd7b | |||
| d7df5161d4 | |||
| d5b677ab68 | |||
| 5b6c5d2076 | |||
| a390e4bd10 | |||
| 82fc0e87a8 | |||
| deec60347b | |||
| 908bee1e7b | |||
| 27d1f60e2c | |||
| d238153a22 | |||
| 914dd4eaa8 | |||
| e79118a0f4 | |||
| 124e1a1a06 | |||
| 00019a8c0d | |||
| 4e0139fef1 | |||
| 9e86a7655e | |||
| aa3691a59f | |||
| f713443c20 | |||
| 2f4c3a15b7 | |||
| 54df84a7da |
@@ -8,13 +8,16 @@ This repository contains the code for the EZGG LAN Manager.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Working Installation of MySQL 5 or latest MariaDB Server (`mariadb-server` for Debian-based Linux, `XAMPP` for Windows)
|
||||
- Working Installation of MariaDB Server (version `10.6.25` or later)
|
||||
+ MySQL should work too, but there are no guarantees.
|
||||
- Python 3.9 or higher
|
||||
- PyCharm or similar IDE (optional)
|
||||
|
||||
### Step 1: Preparing Database
|
||||
|
||||
To prepare the database, apply the SQL file located in `sql/create_database.sql` to your database server. This is easily accomplished with the MYSQL Workbench, but it can be also done by pipeing the file into the mariadb-server executable.
|
||||
To prepare the database, apply the SQL file located in `sql/create_database.sql` to your database server. This is easily accomplished with the MYSQL Workbench, but it can be also done by piping the file into the mariadb-server executable.
|
||||
|
||||
After creating the database, apply all patches found in `sql/*_patch.sql` in their numeric order.
|
||||
|
||||
Optionally, you can now execute the script `create_demo_database_content.py`, found in `src/ezgg_lan_manager/helpers`. Be aware that it can be buggy sometimes, especially if you overwrite existing data.
|
||||
|
||||
@@ -43,3 +46,4 @@ FLUSH PRIVILEGES;
|
||||
```
|
||||
3. Make sure to **NOT** use the default passwords!
|
||||
4. Apply the `create_database.sql` when starting the MariaDB container for the first time.
|
||||
5. Apply the patches (`sql/*_patch.sql`) when starting the MariaDB container for the first time.
|
||||
|
||||
@@ -21,7 +21,7 @@ services:
|
||||
MARIADB_USER: ezgg_lan_manager
|
||||
MARIADB_PASSWORD: Alkohol1
|
||||
healthcheck:
|
||||
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost"]
|
||||
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost", "-pAlkohol1"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
@@ -30,6 +30,7 @@ services:
|
||||
volumes:
|
||||
- database:/var/lib/mysql
|
||||
- ./sql/create_database.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
- ./sql:/sql
|
||||
|
||||
|
||||
volumes:
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
-- Apply this patch after using create_database.sql to extend the schema to support tournaments from version 0.2.0
|
||||
-- WARNING: Executing this on a post 0.2.0 database will delete all data related to tournaments !!!
|
||||
|
||||
DROP TABLE IF EXISTS `game_titles`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `game_titles` (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
web_link VARCHAR(512) NOT NULL,
|
||||
image_name VARCHAR(255) NOT NULL,
|
||||
UNIQUE KEY uq_game_title_name (name)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS `tournaments`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `tournaments` (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
game_title_id INT NOT NULL,
|
||||
format VARCHAR(20) NOT NULL, -- SE_BO1, DE_BO3, ...
|
||||
start_time DATETIME NOT NULL,
|
||||
status VARCHAR(20) NOT NULL, -- OPEN, CLOSED, ONGOING, ...
|
||||
max_participants INT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT fk_tournament_game
|
||||
FOREIGN KEY (game_title_id)
|
||||
REFERENCES game_titles(id)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
CREATE INDEX idx_tournaments_game_title
|
||||
ON tournaments(game_title_id);
|
||||
|
||||
DROP TABLE IF EXISTS `tournament_participants`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `tournament_participants` (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
tournament_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
participant_type VARCHAR(10) NOT NULL DEFAULT 'PLAYER',
|
||||
seed INT NULL,
|
||||
joined_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE KEY uq_tournament_user (tournament_id, user_id),
|
||||
|
||||
CONSTRAINT fk_tp_tournament
|
||||
FOREIGN KEY (tournament_id)
|
||||
REFERENCES tournaments(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_tp_user
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES users(user_id)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
CREATE INDEX idx_tp_tournament
|
||||
ON tournament_participants(tournament_id);
|
||||
CREATE INDEX idx_tp_user
|
||||
ON tournament_participants(user_id);
|
||||
|
||||
DROP TABLE IF EXISTS `tournament_rounds`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `tournament_rounds` (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
tournament_id INT NOT NULL,
|
||||
bracket VARCHAR(10) NOT NULL, -- UPPER, LOWER, FINAL
|
||||
round_index INT NOT NULL,
|
||||
|
||||
UNIQUE KEY uq_round (tournament_id, bracket, round_index),
|
||||
|
||||
CONSTRAINT fk_round_tournament
|
||||
FOREIGN KEY (tournament_id)
|
||||
REFERENCES tournaments(id)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
CREATE INDEX idx_rounds_tournament
|
||||
ON tournament_rounds(tournament_id);
|
||||
|
||||
DROP TABLE IF EXISTS `matches`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `matches` (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
tournament_id INT NOT NULL,
|
||||
round_id INT NOT NULL,
|
||||
match_index INT NOT NULL,
|
||||
status VARCHAR(15) NOT NULL, -- WAITING, PENDING, COMPLETED, ...
|
||||
best_of INT NOT NULL, -- 1, 3, 5
|
||||
scheduled_time DATETIME NULL,
|
||||
completed_at DATETIME NULL,
|
||||
|
||||
UNIQUE KEY uq_match (round_id, match_index),
|
||||
|
||||
CONSTRAINT fk_match_tournament
|
||||
FOREIGN KEY (tournament_id)
|
||||
REFERENCES tournaments(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_match_round
|
||||
FOREIGN KEY (round_id)
|
||||
REFERENCES tournament_rounds(id)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
CREATE INDEX idx_matches_tournament
|
||||
ON matches(tournament_id);
|
||||
|
||||
CREATE INDEX idx_matches_round
|
||||
ON matches(round_id);
|
||||
|
||||
DROP TABLE IF EXISTS `match_participants`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `match_participants` (
|
||||
match_id INT NOT NULL,
|
||||
participant_id INT NOT NULL,
|
||||
score INT NULL,
|
||||
is_winner TINYINT(1) NULL,
|
||||
|
||||
PRIMARY KEY (match_id, participant_id),
|
||||
|
||||
CONSTRAINT fk_mp_match
|
||||
FOREIGN KEY (match_id)
|
||||
REFERENCES matches(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_mp_participant
|
||||
FOREIGN KEY (participant_id)
|
||||
REFERENCES tournament_participants(id)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
@@ -0,0 +1,63 @@
|
||||
-- =====================================================
|
||||
-- Teams
|
||||
-- =====================================================
|
||||
|
||||
DROP TABLE IF EXISTS `team_members`;
|
||||
DROP TABLE IF EXISTS `teams`;
|
||||
|
||||
|
||||
-- -----------------------------------------------------
|
||||
-- Teams table
|
||||
-- -----------------------------------------------------
|
||||
CREATE TABLE `teams` (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
abbreviation VARCHAR(10) NOT NULL,
|
||||
join_password VARCHAR(255) NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE KEY uq_team_name (name),
|
||||
UNIQUE KEY uq_team_abbr (abbreviation)
|
||||
|
||||
) ENGINE=InnoDB
|
||||
DEFAULT CHARSET=utf8mb4
|
||||
COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
-- -----------------------------------------------------
|
||||
-- Team Members (Junction Table)
|
||||
-- -----------------------------------------------------
|
||||
CREATE TABLE `team_members` (
|
||||
team_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
|
||||
status ENUM('MEMBER','OFFICER','LEADER')
|
||||
NOT NULL DEFAULT 'MEMBER',
|
||||
|
||||
joined_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY (team_id, user_id),
|
||||
|
||||
CONSTRAINT fk_tm_team
|
||||
FOREIGN KEY (team_id)
|
||||
REFERENCES teams(id)
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT fk_tm_user
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES users(user_id)
|
||||
ON DELETE CASCADE
|
||||
|
||||
) ENGINE=InnoDB
|
||||
DEFAULT CHARSET=utf8mb4
|
||||
COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
-- -----------------------------------------------------
|
||||
-- Indexes
|
||||
-- -----------------------------------------------------
|
||||
CREATE INDEX idx_tm_user
|
||||
ON team_members(user_id);
|
||||
|
||||
CREATE INDEX idx_tm_team_status
|
||||
ON team_members(team_id, status);
|
||||
@@ -0,0 +1,10 @@
|
||||
-- =====================================================
|
||||
-- Adds type of participant to tournament
|
||||
-- =====================================================
|
||||
|
||||
ALTER TABLE `tournaments` ADD COLUMN `participant_type` ENUM('PLAYER','TEAM') NOT NULL DEFAULT 'PLAYER' AFTER `created_at`;
|
||||
|
||||
ALTER TABLE `tournament_participants`
|
||||
CHANGE COLUMN `user_id` `user_id` INT(11) NULL AFTER `tournament_id`,
|
||||
ADD COLUMN `team_id` INT(11) NULL AFTER `user_id`,
|
||||
ADD CONSTRAINT `fk_tp_team` FOREIGN KEY (`team_id`) REFERENCES `teams` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Apply this patch after using create_database.sql to extend the schema to support fallback passwords
|
||||
|
||||
ALTER TABLE users
|
||||
ADD COLUMN user_fallback_password VARCHAR(255) DEFAULT NULL
|
||||
AFTER user_password;
|
||||
@@ -1,18 +1,17 @@
|
||||
import logging
|
||||
from asyncio import get_event_loop
|
||||
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from rio import App, Theme, Color, Font, ComponentPage, Session
|
||||
from from_root import from_root
|
||||
|
||||
from src.ezgg_lan_manager import pages, init_services
|
||||
from src.ezgg_lan_manager import pages, init_services, LocalDataService
|
||||
from src.ezgg_lan_manager.helpers.LoggedInGuard import logged_in_guard, not_logged_in_guard, team_guard
|
||||
from src.ezgg_lan_manager.services.DatabaseService import NoDatabaseConnectionError
|
||||
from src.ezgg_lan_manager.services.LocalDataService import LocalData
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
logger = logging.getLogger("EzggLanManager")
|
||||
|
||||
@@ -30,14 +29,19 @@ if __name__ == "__main__":
|
||||
corner_radius_large=0,
|
||||
font=Font(from_root("src/ezgg_lan_manager/assets/fonts/joystix.otf"))
|
||||
)
|
||||
default_attachments: list = [LocalData()]
|
||||
default_attachments: list = [LocalData(stored_session_token=None)]
|
||||
default_attachments.extend(init_services())
|
||||
|
||||
lan_info = default_attachments[3].get_lan_info()
|
||||
|
||||
async def on_session_start(session: Session) -> None:
|
||||
# Use this line to fake being any user without having to log in
|
||||
# session.attach(UserSession(id=uuid4(), user_id=30, is_team_member=True))
|
||||
await session.set_title(lan_info.name)
|
||||
session.attach(SessionStorage())
|
||||
if session[LocalData].stored_session_token:
|
||||
user_session = session[LocalDataService].verify_token(session[LocalData].stored_session_token)
|
||||
if user_session is not None:
|
||||
session.attach(user_session)
|
||||
|
||||
async def on_app_start(a: App) -> None:
|
||||
init_result = await a.default_attachments[4].init_db_pool()
|
||||
@@ -157,6 +161,12 @@ if __name__ == "__main__":
|
||||
build=pages.ManageTournamentsPage,
|
||||
guard=team_guard
|
||||
),
|
||||
ComponentPage(
|
||||
name="AdminNavigationPage",
|
||||
url_segment="admin",
|
||||
build=pages.AdminNavigationPage,
|
||||
guard=team_guard
|
||||
),
|
||||
ComponentPage(
|
||||
name="DbErrorPage",
|
||||
url_segment="db-error",
|
||||
@@ -166,6 +176,21 @@ if __name__ == "__main__":
|
||||
name="TournamentDetailsPage",
|
||||
url_segment="tournament",
|
||||
build=pages.TournamentDetailsPage,
|
||||
),
|
||||
ComponentPage(
|
||||
name="TournamentRulesPage",
|
||||
url_segment="tournament-rules",
|
||||
build=pages.TournamentRulesPage,
|
||||
),
|
||||
ComponentPage(
|
||||
name="Teams",
|
||||
url_segment="teams",
|
||||
build=pages.TeamsPage,
|
||||
),
|
||||
ComponentPage(
|
||||
name="ConwaysGameOfLife",
|
||||
url_segment="conway",
|
||||
build=pages.ConwayPage,
|
||||
)
|
||||
],
|
||||
theme=theme,
|
||||
@@ -193,5 +218,5 @@ if __name__ == "__main__":
|
||||
|
||||
sys.exit(app.run_as_web_server(
|
||||
host="0.0.0.0",
|
||||
port=8001,
|
||||
port=8000,
|
||||
))
|
||||
|
||||
@@ -10,15 +10,17 @@ from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
||||
from src.ezgg_lan_manager.services.LocalDataService import LocalDataService
|
||||
from src.ezgg_lan_manager.services.MailingService import MailingService
|
||||
from src.ezgg_lan_manager.services.NewsService import NewsService
|
||||
from src.ezgg_lan_manager.services.RefreshService import RefreshService
|
||||
from src.ezgg_lan_manager.services.ReceiptPrintingService import ReceiptPrintingService
|
||||
from src.ezgg_lan_manager.services.SeatingService import SeatingService
|
||||
from src.ezgg_lan_manager.services.TeamService import TeamService
|
||||
from src.ezgg_lan_manager.services.TicketingService import TicketingService
|
||||
from src.ezgg_lan_manager.services.TournamentService import TournamentService
|
||||
from src.ezgg_lan_manager.services.UserService import UserService
|
||||
from src.ezgg_lan_manager.types import *
|
||||
|
||||
# Inits services in the correct order
|
||||
def init_services() -> tuple[AccountingService, CateringService, ConfigurationService, DatabaseService, MailingService, NewsService, SeatingService, TicketingService, UserService, LocalDataService, ReceiptPrintingService, TournamentService]:
|
||||
def init_services() -> tuple[AccountingService, CateringService, ConfigurationService, DatabaseService, MailingService, NewsService, SeatingService, TicketingService, UserService, LocalDataService, ReceiptPrintingService, TournamentService, TeamService, RefreshService]:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
configuration_service = ConfigurationService(from_root("config.toml"))
|
||||
db_service = DatabaseService(configuration_service.get_database_configuration())
|
||||
@@ -32,6 +34,7 @@ def init_services() -> tuple[AccountingService, CateringService, ConfigurationSe
|
||||
catering_service = CateringService(db_service, accounting_service, user_service, receipt_printing_service)
|
||||
local_data_service = LocalDataService()
|
||||
tournament_service = TournamentService(db_service, user_service)
|
||||
team_service = TeamService(db_service)
|
||||
refresh_service = RefreshService()
|
||||
|
||||
|
||||
return accounting_service, catering_service, configuration_service, db_service, mailing_service, news_service, seating_service, ticketing_service, user_service, local_data_service, receipt_printing_service, tournament_service
|
||||
return accounting_service, catering_service, configuration_service, db_service, mailing_service, news_service, seating_service, ticketing_service, user_service, local_data_service, receipt_printing_service, tournament_service, team_service, refresh_service
|
||||
|
||||
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,36 @@
|
||||
from rio import Component, Rectangle, Text, Column, Icon, TextStyle, PointerEventListener, PointerEvent
|
||||
|
||||
|
||||
class AdminNavigationCard(Component):
|
||||
icon_name: str
|
||||
display_text: str
|
||||
target_url: str
|
||||
|
||||
def on_press(self, _: PointerEvent) -> None:
|
||||
if self.target_url:
|
||||
self.session.navigate_to(self.target_url)
|
||||
|
||||
def build(self) -> Component:
|
||||
return PointerEventListener(
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Icon(
|
||||
self.icon_name,
|
||||
min_width=3.5,
|
||||
min_height=3.5,
|
||||
fill="background",
|
||||
margin=1
|
||||
),
|
||||
Text(self.display_text, style=TextStyle(fill=self.session.theme.background_color), justify="center", margin=1, margin_top=0)
|
||||
),
|
||||
cursor="pointer",
|
||||
stroke_width=0.2,
|
||||
stroke_color=self.session.theme.background_color,
|
||||
hover_stroke_width=0.2,
|
||||
hover_stroke_color=self.session.theme.hud_color,
|
||||
min_width=10,
|
||||
min_height=10,
|
||||
corner_radius=0.2
|
||||
),
|
||||
on_press=self.on_press
|
||||
)
|
||||
@@ -1,38 +0,0 @@
|
||||
from asyncio import sleep
|
||||
|
||||
from rio import Text, Component, TextStyle
|
||||
|
||||
|
||||
class AnimatedText(Component):
|
||||
def __post_init__(self) -> None:
|
||||
self._display_printing: list[bool] = [False]
|
||||
self.text_comp = Text("")
|
||||
|
||||
async def display_text(self, success: bool, text: str, speed: float = 0.06, font_size: float = 0.9) -> None:
|
||||
if self._display_printing[0]:
|
||||
return
|
||||
else:
|
||||
self._display_printing[0] = True
|
||||
self.text_comp.text = ""
|
||||
if success:
|
||||
self.text_comp.style = TextStyle(
|
||||
fill=self.session.theme.success_color,
|
||||
font_size=font_size
|
||||
)
|
||||
for c in text:
|
||||
self.text_comp.text = self.text_comp.text + c
|
||||
self.text_comp.force_refresh()
|
||||
await sleep(speed)
|
||||
else:
|
||||
self.text_comp.style = TextStyle(
|
||||
fill=self.session.theme.danger_color,
|
||||
font_size=font_size
|
||||
)
|
||||
for c in text:
|
||||
self.text_comp.text = self.text_comp.text + c
|
||||
self.text_comp.force_refresh()
|
||||
await sleep(speed)
|
||||
self._display_printing[0] = False
|
||||
|
||||
def build(self) -> Component:
|
||||
return self.text_comp
|
||||
@@ -1,7 +1,6 @@
|
||||
from typing import Callable
|
||||
|
||||
from rio import Component, Row, Text, TextStyle, Color, Rectangle, CursorStyle
|
||||
from rio.components.pointer_event_listener import PointerEvent, PointerEventListener
|
||||
from rio import Component, Row, Text, TextStyle, Color, Rectangle, PointerEventListener
|
||||
|
||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrderStatus, CateringOrder
|
||||
|
||||
@@ -41,7 +40,7 @@ class CateringOrderItem(Component):
|
||||
fill=self.session.theme.primary_color,
|
||||
hover_fill=self.session.theme.hud_color,
|
||||
transition_time=0.1,
|
||||
cursor=CursorStyle.POINTER
|
||||
cursor="pointer"
|
||||
),
|
||||
on_press=lambda _: self.info_modal_cb(self.order),
|
||||
)
|
||||
|
||||
@@ -46,10 +46,10 @@ class CateringSelectionItem(Component):
|
||||
Text(AccountingService.make_euro_string_from_decimal(self.article_price),
|
||||
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
|
||||
IconButton(
|
||||
icon="material/add",
|
||||
icon="material/add" if self.is_sensitive else "material/do_not_disturb_on_total_silence",
|
||||
min_size=2,
|
||||
color=self.session.theme.success_color,
|
||||
style="plain-text",
|
||||
color=self.session.theme.success_color if self.is_sensitive else self.session.theme.danger_color,
|
||||
style="colored-text",
|
||||
on_press=lambda: self.on_add_callback(self.article_id),
|
||||
is_sensitive=self.is_sensitive
|
||||
),
|
||||
|
||||
@@ -1,88 +1,58 @@
|
||||
from copy import copy, deepcopy
|
||||
from typing import Optional, Callable
|
||||
|
||||
from rio import *
|
||||
from rio import Component, event, Spacer, Card, Column, Text, TextStyle
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService, UserService, LocalDataService
|
||||
from src.ezgg_lan_manager.services.ConfigurationService import ConfigurationService
|
||||
from src.ezgg_lan_manager.services.UserService import UserService
|
||||
from src.ezgg_lan_manager.components.DesktopNavigationButton import DesktopNavigationButton
|
||||
from src.ezgg_lan_manager.components.NavigationSponsorBox import NavigationSponsorBox
|
||||
from src.ezgg_lan_manager.components.UserInfoAndLoginBox import UserInfoAndLoginBox
|
||||
from src.ezgg_lan_manager.services.LocalDataService import LocalData
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
|
||||
class DesktopNavigation(Component):
|
||||
user: Optional[User] = None
|
||||
force_login_box_refresh: list[Callable] = []
|
||||
|
||||
@event.on_populate
|
||||
async def async_init(self) -> None:
|
||||
self.session[SessionStorage].subscribe_to_logged_in_or_out_event(str(self.__class__), self.async_init)
|
||||
local_data = self.session[LocalData]
|
||||
if local_data.stored_session_token:
|
||||
session_ = self.session[LocalDataService].verify_token(local_data.stored_session_token)
|
||||
if session_:
|
||||
self.session.detach(SessionStorage)
|
||||
self.session.attach(session_)
|
||||
self.user = await self.session[UserService].get_user(session_.user_id)
|
||||
try:
|
||||
# Hack-around, maybe fix in the future
|
||||
self.force_login_box_refresh[-1]()
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
return
|
||||
|
||||
if self.session[SessionStorage].user_id:
|
||||
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||
else:
|
||||
async def on_populate(self) -> None:
|
||||
try:
|
||||
self.user = await self.session[UserService].get_user(self.session[UserSession].user_id)
|
||||
except KeyError:
|
||||
self.user = None
|
||||
|
||||
def build(self) -> Component:
|
||||
lan_info = self.session[ConfigurationService].get_lan_info()
|
||||
user_info_and_login_box = UserInfoAndLoginBox()
|
||||
self.force_login_box_refresh.append(user_info_and_login_box.force_refresh)
|
||||
user_navigation = [
|
||||
user_info_and_login_box = UserInfoAndLoginBox(state_changed_cb=self.on_populate)
|
||||
navigation = [
|
||||
DesktopNavigationButton("News", "./news"),
|
||||
Spacer(min_height=1),
|
||||
Spacer(min_height=0.7),
|
||||
DesktopNavigationButton(f"Über {lan_info.name} {lan_info.iteration}", "./overview"),
|
||||
DesktopNavigationButton("Ticket kaufen", "./buy_ticket"),
|
||||
DesktopNavigationButton("Sitzplan", "./seating"),
|
||||
DesktopNavigationButton("Catering", "./catering"),
|
||||
DesktopNavigationButton("Teilnehmer", "./guests"),
|
||||
DesktopNavigationButton("Teams", "./teams"),
|
||||
DesktopNavigationButton("Turniere", "./tournaments"),
|
||||
DesktopNavigationButton("FAQ", "./faq"),
|
||||
DesktopNavigationButton("Regeln & AGB", "./rules-gtc"),
|
||||
Spacer(min_height=1),
|
||||
Spacer(min_height=0.7),
|
||||
DesktopNavigationButton("Discord", "https://discord.gg/8gTjg34yyH", open_new_tab=True),
|
||||
DesktopNavigationButton("Die EZ GG e.V.", "https://ezgg-ev.de/about", open_new_tab=True),
|
||||
DesktopNavigationButton("Kontakt", "./contact"),
|
||||
DesktopNavigationButton("Impressum & DSGVO", "./imprint"),
|
||||
Spacer(min_height=1)
|
||||
Spacer(min_height=0.7)
|
||||
]
|
||||
team_navigation = [
|
||||
Text("Verwaltung", align_x=0.5, margin_top=0.3, style=TextStyle(fill=Color.from_hex("F0EADE"), font_size=1.2)),
|
||||
Text("Vorsichtig sein!", align_x=0.5, margin_top=0.3, style=TextStyle(fill=self.session.theme.danger_color, font_size=0.6)),
|
||||
DesktopNavigationButton("News", "./manage-news", is_team_navigation=True),
|
||||
DesktopNavigationButton("Benutzer", "./manage-users", is_team_navigation=True),
|
||||
DesktopNavigationButton("Catering", "./manage-catering", is_team_navigation=True),
|
||||
DesktopNavigationButton("Turniere", "./manage-tournaments", is_team_navigation=True),
|
||||
Spacer(min_height=1),
|
||||
Revealer(
|
||||
header="Normale Navigation",
|
||||
content=Column(*user_navigation),
|
||||
header_style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9)
|
||||
)
|
||||
] if self.user is not None and self.user.is_team_member else []
|
||||
|
||||
nav_to_use = copy(team_navigation) if self.user is not None and self.user.is_team_member else copy(user_navigation)
|
||||
if self.user is not None and self.user.is_team_member:
|
||||
navigation.insert(0, DesktopNavigationButton("Adminbereich", "./admin", is_team_navigation=True))
|
||||
|
||||
return Card(
|
||||
Column(
|
||||
Text(lan_info.name, align_x=0.5, margin_top=0.3, style=TextStyle(fill=self.session.theme.hud_color, font_size=1.9)),
|
||||
Text(f"Edition {lan_info.iteration}", align_x=0.5, style=TextStyle(fill=self.session.theme.hud_color, font_size=1.2), margin_top=0.3, margin_bottom=2),
|
||||
user_info_and_login_box,
|
||||
*nav_to_use,
|
||||
*navigation,
|
||||
Text("Unsere Sponsoren", align_x=0.5, style=TextStyle(fill=self.session.theme.hud_color, font_size=0.9), margin_bottom=0.5, margin_top=1),
|
||||
NavigationSponsorBox(img_name="crackz", url="https://www.crackz.gg/"),
|
||||
align_y=0
|
||||
),
|
||||
color=self.session.theme.neutral_color,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
from rio import Component, TextStyle, Color, TextInput, Button, Text, Rectangle, Column, Row, Spacer, \
|
||||
EventHandler
|
||||
import uuid
|
||||
|
||||
from rio import Component, TextStyle, Color, TextInput, Button, Text, Rectangle, Column, Row, Spacer, \
|
||||
EventHandler, Webview
|
||||
|
||||
from src.ezgg_lan_manager import RefreshService
|
||||
from src.ezgg_lan_manager.services.LocalDataService import LocalDataService, LocalData
|
||||
from src.ezgg_lan_manager.services.UserService import UserService
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
|
||||
class LoginBox(Component):
|
||||
@@ -26,11 +29,13 @@ class LoginBox(Component):
|
||||
self.password_input_is_valid = True
|
||||
self.login_button_is_loading = False
|
||||
self.is_account_locked = False
|
||||
await self.session[SessionStorage].set_user_id_and_team_member_flag(user.user_id, user.is_team_member)
|
||||
token = self.session[LocalDataService].set_session(self.session[SessionStorage])
|
||||
user_session = UserSession(id=uuid.uuid4(), user_id=user.user_id, is_team_member=user.is_team_member)
|
||||
self.session.attach(user_session)
|
||||
token = self.session[LocalDataService].set_session(user_session)
|
||||
self.session[LocalData].stored_session_token = token
|
||||
self.session.attach(self.session[LocalData])
|
||||
self.status_change_cb()
|
||||
await self.status_change_cb()
|
||||
await self.session[RefreshService].trigger_refresh()
|
||||
else:
|
||||
self.user_name_input_is_valid = False
|
||||
self.password_input_is_valid = False
|
||||
@@ -102,5 +107,5 @@ class LoginBox(Component):
|
||||
min_width=12,
|
||||
align_x=0.5,
|
||||
margin_top=0.3,
|
||||
margin_bottom=2
|
||||
)
|
||||
margin_bottom=1.5
|
||||
)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from from_root import from_root
|
||||
from rio import Component, Link, Rectangle, Image, Color
|
||||
|
||||
|
||||
class NavigationSponsorBox(Component):
|
||||
img_name: str
|
||||
url: str
|
||||
img_suffix: str = "png"
|
||||
|
||||
def build(self) -> Component:
|
||||
return Link(
|
||||
content=Rectangle(
|
||||
content=Image(image=from_root(f"src/ezgg_lan_manager/assets/img/{self.img_name}.{self.img_suffix}"), min_width=10, min_height=10),
|
||||
stroke_width=0.1,
|
||||
stroke_color=Color.TRANSPARENT,
|
||||
hover_stroke_width=0.1,
|
||||
hover_stroke_color=self.session.theme.secondary_color,
|
||||
margin=0.6,
|
||||
cursor="pointer"
|
||||
),
|
||||
target_url=self.url,
|
||||
open_in_new_tab=True
|
||||
)
|
||||
@@ -1,11 +1,10 @@
|
||||
from decimal import Decimal
|
||||
from functools import partial
|
||||
from typing import Optional, Callable
|
||||
|
||||
from rio import Component, Column, Text, TextStyle, Button, Spacer, event
|
||||
|
||||
from src.ezgg_lan_manager import TicketingService
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
|
||||
class SeatingPlanInfoBox(Component):
|
||||
@@ -22,11 +21,14 @@ class SeatingPlanInfoBox(Component):
|
||||
|
||||
@event.on_populate
|
||||
async def check_ticket(self) -> None:
|
||||
if self.session[SessionStorage].user_id:
|
||||
user_ticket = await self.session[TicketingService].get_user_ticket(self.session[SessionStorage].user_id)
|
||||
try:
|
||||
user_id = self.session[UserSession].user_id
|
||||
user_ticket = await self.session[TicketingService].get_user_ticket(user_id)
|
||||
self.has_user_ticket = not (user_ticket is None)
|
||||
self.booking_button_text = "Buchen" if self.has_user_ticket else "Ticket kaufen"
|
||||
self.force_refresh()
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
async def purchase_clicked(self):
|
||||
if self.has_user_ticket:
|
||||
@@ -35,6 +37,11 @@ class SeatingPlanInfoBox(Component):
|
||||
self.session.navigate_to("./buy_ticket")
|
||||
|
||||
def build(self) -> Component:
|
||||
try:
|
||||
user_id = self.session[UserSession].user_id
|
||||
except KeyError:
|
||||
user_id = None
|
||||
|
||||
if self.override_text:
|
||||
return Column(Text(self.override_text, margin=1,
|
||||
style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.4), overflow="wrap",
|
||||
@@ -75,9 +82,9 @@ class SeatingPlanInfoBox(Component):
|
||||
grow_y=False,
|
||||
is_sensitive=not self.is_booking_blocked,
|
||||
on_press=self.purchase_clicked
|
||||
) if self.session[SessionStorage].user_id else Text(f"Du musst eingeloggt sein um einen Sitzplatz zu buchen",
|
||||
margin=1,
|
||||
style=TextStyle(fill=self.session.theme.neutral_color),
|
||||
overflow="wrap", justify="center"),
|
||||
) if user_id is not None else Text(f"Du musst eingeloggt sein um einen Sitzplatz zu buchen",
|
||||
margin=1,
|
||||
style=TextStyle(fill=self.session.theme.neutral_color),
|
||||
overflow="wrap", justify="center"),
|
||||
min_height=10
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ from rio import Component, Text, Icon, TextStyle, Rectangle, Spacer, Color, Poin
|
||||
from typing import Optional, Callable, Literal
|
||||
|
||||
from src.ezgg_lan_manager.types.Seat import Seat
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
|
||||
class SeatPixel(Component):
|
||||
@@ -14,7 +14,11 @@ class SeatPixel(Component):
|
||||
seat_orientation: Literal["top", "bottom"]
|
||||
|
||||
def determine_color(self) -> Color:
|
||||
if self.seat.user is not None and self.seat.user.user_id == self.session[SessionStorage].user_id:
|
||||
try:
|
||||
user_id = self.session[UserSession].user_id
|
||||
except KeyError:
|
||||
user_id = None
|
||||
if self.seat.user is not None and self.seat.user.user_id == user_id:
|
||||
return Color.from_hex("800080")
|
||||
elif self.seat.is_blocked or self.seat.user is not None:
|
||||
return self.session.theme.danger_color
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
from asyncio import sleep, create_task
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
|
||||
import rio
|
||||
from rio import Component, Column, Text, TextStyle, Button, Row, ScrollContainer, Spacer, Popup, Table, event
|
||||
from rio import Component, Column, Text, TextStyle, Button, Row, ScrollContainer, Spacer, Popup, Table, event, Card
|
||||
|
||||
from src.ezgg_lan_manager.components.CateringCartItem import CateringCartItem
|
||||
from src.ezgg_lan_manager.components.CateringOrderItem import CateringOrderItem
|
||||
from src.ezgg_lan_manager.services.AccountingService import AccountingService
|
||||
from src.ezgg_lan_manager.services.CateringService import CateringService, CateringError, CateringErrorType
|
||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringMenuItemsWithAmount
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
POPUP_CLOSE_TIMEOUT_SECONDS = 3
|
||||
|
||||
@@ -23,16 +23,21 @@ class ShoppingCartAndOrders(Component):
|
||||
|
||||
@event.periodic(5)
|
||||
async def periodic_refresh_of_orders(self) -> None:
|
||||
if not self.show_cart and not self.popup_is_shown:
|
||||
self.orders = await self.session[CateringService].get_orders_for_user(self.session[SessionStorage].user_id)
|
||||
user_id = self._get_user_id()
|
||||
if not self.show_cart and not self.popup_is_shown and user_id is not None:
|
||||
self.orders = await self.session[CateringService].get_orders_for_user(user_id)
|
||||
|
||||
async def switch(self) -> None:
|
||||
self.show_cart = not self.show_cart
|
||||
self.orders = await self.session[CateringService].get_orders_for_user(self.session[SessionStorage].user_id)
|
||||
user_id = self._get_user_id()
|
||||
if user_id is not None:
|
||||
self.orders = await self.session[CateringService].get_orders_for_user(user_id)
|
||||
|
||||
async def on_remove_item(self, list_id: int) -> None:
|
||||
catering_service = self.session[CateringService]
|
||||
user_id = self.session[SessionStorage].user_id
|
||||
user_id = self._get_user_id()
|
||||
if user_id is None:
|
||||
return
|
||||
cart = catering_service.get_cart(user_id)
|
||||
try:
|
||||
cart.pop(list_id)
|
||||
@@ -42,13 +47,16 @@ class ShoppingCartAndOrders(Component):
|
||||
self.force_refresh()
|
||||
|
||||
async def on_empty_cart_pressed(self) -> None:
|
||||
self.session[CateringService].save_cart(self.session[SessionStorage].user_id, [])
|
||||
user_id = self._get_user_id()
|
||||
if user_id is None:
|
||||
return
|
||||
self.session[CateringService].save_cart(user_id, [])
|
||||
self.force_refresh()
|
||||
|
||||
async def on_add_item(self, article_id: int) -> None:
|
||||
catering_service = self.session[CateringService]
|
||||
user_id = self.session[SessionStorage].user_id
|
||||
if not user_id:
|
||||
user_id = self._get_user_id()
|
||||
if user_id is None:
|
||||
return
|
||||
cart = catering_service.get_cart(user_id)
|
||||
item_to_add = await catering_service.get_menu_item_by_id(article_id)
|
||||
@@ -69,7 +77,9 @@ class ShoppingCartAndOrders(Component):
|
||||
self.order_button_loading = True
|
||||
self.force_refresh()
|
||||
|
||||
user_id = self.session[SessionStorage].user_id
|
||||
user_id = self._get_user_id()
|
||||
if user_id is None:
|
||||
return
|
||||
cart = self.session[CateringService].get_cart(user_id)
|
||||
show_popup_task = None
|
||||
if len(cart) < 1:
|
||||
@@ -91,13 +101,13 @@ class ShoppingCartAndOrders(Component):
|
||||
else:
|
||||
show_popup_task = create_task(self.show_popup("Unbekannter Fehler", True))
|
||||
else:
|
||||
self.session[CateringService].save_cart(self.session[SessionStorage].user_id, [])
|
||||
self.session[CateringService].save_cart(user_id, [])
|
||||
self.order_button_loading = False
|
||||
if not show_popup_task:
|
||||
show_popup_task = create_task(self.show_popup("Bestellung erfolgreich aufgegeben!", False))
|
||||
|
||||
async def _create_order_info_modal(self, order: CateringOrder) -> None:
|
||||
def build_dialog_content() -> rio.Component:
|
||||
def build_dialog_content() -> Component:
|
||||
# @todo: rio 0.10.8 did not have the ability to align the columns, check back in a future version
|
||||
table = Table(
|
||||
{
|
||||
@@ -107,9 +117,9 @@ class ShoppingCartAndOrders(Component):
|
||||
},
|
||||
show_row_numbers=False
|
||||
)
|
||||
return rio.Card(
|
||||
rio.Column(
|
||||
rio.Text(
|
||||
return Card(
|
||||
Column(
|
||||
Text(
|
||||
f"Deine Bestellung ({order.order_id})",
|
||||
align_x=0.5,
|
||||
margin_bottom=0.5
|
||||
@@ -134,10 +144,16 @@ class ShoppingCartAndOrders(Component):
|
||||
)
|
||||
await dialog.wait_for_close()
|
||||
|
||||
def build(self) -> rio.Component:
|
||||
user_id = self.session[SessionStorage].user_id
|
||||
def _get_user_id(self) -> Optional[int]:
|
||||
try:
|
||||
return self.session[UserSession].user_id
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def build(self) -> Component:
|
||||
user_id = self._get_user_id()
|
||||
catering_service = self.session[CateringService]
|
||||
cart = catering_service.get_cart(user_id)
|
||||
cart = catering_service.get_cart(user_id) if user_id is not None else []
|
||||
if self.show_cart:
|
||||
cart_container = ScrollContainer(
|
||||
content=Column(
|
||||
@@ -155,7 +171,6 @@ class ShoppingCartAndOrders(Component):
|
||||
margin=1
|
||||
)
|
||||
return Column(
|
||||
cart_container,
|
||||
Popup(
|
||||
anchor=cart_container,
|
||||
content=Text(self.popup_message, style=TextStyle(fill=self.session.theme.danger_color if self.popup_is_error else self.session.theme.success_color), overflow="wrap", margin=2, justify="center", min_width=20),
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
from functools import partial
|
||||
from typing import Callable, Optional, Literal
|
||||
|
||||
from rio import Component, Revealer, TextStyle, Column, Row, Tooltip, Icon, Spacer, Text, Button
|
||||
|
||||
from src.ezgg_lan_manager.types.Team import TeamStatus, Team
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
|
||||
class TeamRevealer(Component):
|
||||
user: Optional[User]
|
||||
team: Team
|
||||
mode: Literal["join", "leave", "display"]
|
||||
on_button_pressed: Callable
|
||||
|
||||
def build(self) -> Component:
|
||||
return Revealer(
|
||||
header=self.team.name,
|
||||
header_style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1
|
||||
),
|
||||
content=Column(
|
||||
*[Row(
|
||||
Tooltip(
|
||||
anchor=Icon("material/star" if self.team.members[member] == TeamStatus.LEADER else "material/stat_1", fill=self.session.theme.hud_color),
|
||||
tip="Leiter" if self.team.members[member] == TeamStatus.LEADER else "Mitglied", position="top"),
|
||||
Text(member.user_name, style=TextStyle(fill=self.session.theme.background_color, font_size=1), margin_left=0.5),
|
||||
Spacer(grow_y=False))
|
||||
for member in self.team.members
|
||||
],
|
||||
Row(Button(
|
||||
content=f"{self.team.name} beitreten" if self.mode == "join" else f"{self.team.name} verlassen",
|
||||
shape="rectangle",
|
||||
style="major",
|
||||
color="hud",
|
||||
on_press=partial(self.on_button_pressed, self.team),
|
||||
), margin_top=1, margin_bottom=1),
|
||||
margin_right=1,
|
||||
margin_left=1
|
||||
),
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
)
|
||||
@@ -0,0 +1,222 @@
|
||||
import logging
|
||||
from typing import Optional, Callable
|
||||
|
||||
from rio import Component, Text, Spacer, Rectangle, Column, TextStyle, Row, Button, TextInput, ThemeContextSwitcher
|
||||
|
||||
from src.ezgg_lan_manager.services.TeamService import TeamService, NotMemberError, TeamLeadRemovalError, AlreadyMemberError, NameNotAllowedError, TeamNameTooLongError, \
|
||||
TeamAbbrInvalidError, TeamNameAlreadyTaken
|
||||
from src.ezgg_lan_manager.types.Team import Team
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
|
||||
class ErrorBox(Component):
|
||||
error_message: str
|
||||
cancel: Callable
|
||||
|
||||
def build(self) -> Component:
|
||||
return Rectangle(
|
||||
content=Column(
|
||||
Text(self.error_message, style=TextStyle(fill=self.session.theme.background_color), margin_bottom=1.5),
|
||||
Row(
|
||||
Button(
|
||||
content="Ok",
|
||||
shape="rectangle",
|
||||
style="major",
|
||||
color="hud",
|
||||
on_press=self.cancel,
|
||||
)
|
||||
),
|
||||
margin=1
|
||||
),
|
||||
fill=self.session.theme.primary_color
|
||||
)
|
||||
|
||||
|
||||
class TeamsDialogJoinHandler(Component):
|
||||
is_active: bool
|
||||
cancel: Callable
|
||||
user: Optional[User] = None
|
||||
team: Optional[Team] = None
|
||||
error_message: Optional[str] = None
|
||||
password: str = ""
|
||||
|
||||
async def join(self) -> None:
|
||||
if self.user is None or self.team is None:
|
||||
return
|
||||
|
||||
if self.password != self.team.join_password:
|
||||
self.error_message = "Falsches Passwort!"
|
||||
return
|
||||
|
||||
try:
|
||||
await self.session[TeamService].add_member_to_team(self.team, self.user)
|
||||
except AlreadyMemberError:
|
||||
self.error_message = "Du bist bereits Mitglied dieses Teams"
|
||||
else:
|
||||
await self.cancel_with_reset()
|
||||
|
||||
async def cancel_with_reset(self) -> None:
|
||||
await self.cancel()
|
||||
self.error_message = None
|
||||
self.password = ""
|
||||
|
||||
def build(self) -> Component:
|
||||
if not self.is_active or self.user is None or self.team is None:
|
||||
return Spacer()
|
||||
|
||||
if self.error_message is not None:
|
||||
return ErrorBox(error_message=self.error_message, cancel=self.cancel_with_reset)
|
||||
|
||||
return Rectangle(
|
||||
content=Column(
|
||||
Text(f"Team {self.team.name} beitreten", style=TextStyle(fill=self.session.theme.background_color), margin_bottom=1, justify="center"),
|
||||
ThemeContextSwitcher(content=TextInput(text=self.bind().password, label="Beitrittspasswort", margin_bottom=1), color="secondary"),
|
||||
Row(
|
||||
Button(
|
||||
content="Abbrechen",
|
||||
shape="rectangle",
|
||||
style="major",
|
||||
color=self.session.theme.danger_color,
|
||||
on_press=self.cancel_with_reset,
|
||||
),
|
||||
Button(
|
||||
content="Beitreten",
|
||||
shape="rectangle",
|
||||
style="major",
|
||||
color=self.session.theme.success_color,
|
||||
on_press=self.join,
|
||||
),
|
||||
spacing=1
|
||||
),
|
||||
margin=1
|
||||
),
|
||||
fill=self.session.theme.primary_color
|
||||
)
|
||||
|
||||
|
||||
class TeamsDialogLeaveHandler(Component):
|
||||
is_active: bool
|
||||
cancel: Callable
|
||||
user: Optional[User] = None
|
||||
team: Optional[Team] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
async def leave(self) -> None:
|
||||
if self.user is not None and self.team is not None:
|
||||
try:
|
||||
await self.session[TeamService].remove_member_from_team(self.team, self.user)
|
||||
except NotMemberError:
|
||||
self.error_message = "Du bist kein Mitglied in diesem Team"
|
||||
except TeamLeadRemovalError:
|
||||
self.error_message = "Als Teamleiter kannst du das Team nicht verlassen"
|
||||
else:
|
||||
await self.cancel_with_reset()
|
||||
|
||||
async def cancel_with_reset(self) -> None:
|
||||
await self.cancel()
|
||||
self.error_message = None
|
||||
|
||||
def build(self) -> Component:
|
||||
if not self.is_active or self.user is None or self.team is None:
|
||||
return Spacer()
|
||||
|
||||
if self.error_message is not None:
|
||||
return ErrorBox(error_message=self.error_message, cancel=self.cancel_with_reset)
|
||||
|
||||
return Rectangle(
|
||||
content=Column(
|
||||
Text(f"Team {self.team.name} wirklich verlassen?", style=TextStyle(fill=self.session.theme.background_color), margin_bottom=1.5, justify="center"),
|
||||
Row(
|
||||
Button(
|
||||
content="Nein",
|
||||
shape="rectangle",
|
||||
style="major",
|
||||
color=self.session.theme.danger_color,
|
||||
on_press=self.cancel_with_reset,
|
||||
),
|
||||
Button(
|
||||
content="Ja",
|
||||
shape="rectangle",
|
||||
style="major",
|
||||
color=self.session.theme.success_color,
|
||||
on_press=self.leave,
|
||||
),
|
||||
spacing=1
|
||||
),
|
||||
margin=1
|
||||
),
|
||||
fill=self.session.theme.primary_color
|
||||
)
|
||||
|
||||
|
||||
class TeamsDialogCreateHandler(Component):
|
||||
is_active: bool
|
||||
cancel: Callable
|
||||
user: Optional[User] = None
|
||||
error_message: Optional[str] = None
|
||||
team_name: str = ""
|
||||
team_abbr: str = ""
|
||||
team_join_password: str = ""
|
||||
|
||||
async def cancel_with_reset(self) -> None:
|
||||
await self.cancel()
|
||||
self.error_message = None
|
||||
self.team_name, self.team_abbr, self.team_join_password = "", "", ""
|
||||
|
||||
async def create(self) -> None:
|
||||
if self.user is None:
|
||||
return
|
||||
|
||||
if not self.team_name or not self.team_abbr or not self.team_join_password:
|
||||
self.error_message = "Angaben unvollständig"
|
||||
return
|
||||
|
||||
try:
|
||||
await self.session[TeamService].create_team(self.team_name, self.team_abbr, self.team_join_password, self.user)
|
||||
except NameNotAllowedError as e:
|
||||
self.error_message = f"Angaben ungültig. Darf kein '{e.disallowed_char}' enthalten."
|
||||
except TeamNameTooLongError:
|
||||
self.error_message = f"Name zu lang. Maximal {TeamService.MAX_TEAM_NAME_LENGTH} Zeichen."
|
||||
except TeamAbbrInvalidError:
|
||||
self.error_message = f"Name zu lang. Maximal {TeamService.MAX_TEAM_ABBR_LENGTH} Zeichen."
|
||||
except TeamNameAlreadyTaken:
|
||||
self.error_message = "Ein Team mit diesem Namen existiert bereits."
|
||||
else:
|
||||
await self.cancel_with_reset()
|
||||
|
||||
def build(self) -> Component:
|
||||
if not self.is_active or self.user is None:
|
||||
return Spacer()
|
||||
|
||||
if self.error_message is not None:
|
||||
return ErrorBox(error_message=self.error_message, cancel=self.cancel_with_reset)
|
||||
|
||||
return Rectangle(
|
||||
content=Column(
|
||||
Text(f"Team gründen", style=TextStyle(fill=self.session.theme.background_color), margin_bottom=1.5, justify="center"),
|
||||
ThemeContextSwitcher(content=TextInput(text=self.bind().team_name, label="Team Name", margin_bottom=1), color="secondary"),
|
||||
ThemeContextSwitcher(content=TextInput(text=self.bind().team_abbr, label="Team Abkürzung", margin_bottom=1), color="secondary"),
|
||||
ThemeContextSwitcher(content=TextInput(text=self.bind().team_join_password, label="Beitrittspasswort", margin_bottom=1), color="secondary"),
|
||||
Row(
|
||||
Button(
|
||||
content="Abbrechen",
|
||||
shape="rectangle",
|
||||
style="major",
|
||||
color=self.session.theme.danger_color,
|
||||
on_press=self.cancel_with_reset,
|
||||
),
|
||||
Button(
|
||||
content="Gründen",
|
||||
shape="rectangle",
|
||||
style="major",
|
||||
color=self.session.theme.success_color,
|
||||
on_press=self.create,
|
||||
),
|
||||
spacing=1
|
||||
),
|
||||
margin=1
|
||||
),
|
||||
fill=self.session.theme.primary_color
|
||||
)
|
||||
@@ -2,7 +2,6 @@ from functools import partial
|
||||
from typing import Callable, Optional
|
||||
from decimal import Decimal
|
||||
|
||||
import rio
|
||||
from rio import Component, Card, Column, Text, Row, Button, TextStyle, ProgressBar, event, Spacer
|
||||
|
||||
from src.ezgg_lan_manager import TicketingService
|
||||
@@ -22,10 +21,10 @@ class TicketBuyCard(Component):
|
||||
available_tickets: int = 0
|
||||
|
||||
@event.on_populate
|
||||
async def async_init(self) -> None:
|
||||
async def on_populate(self) -> None:
|
||||
self.available_tickets = await self.session[TicketingService].get_available_tickets_for_category(self.category)
|
||||
|
||||
def build(self) -> rio.Component:
|
||||
def build(self) -> Component:
|
||||
ticket_description_style = TextStyle(
|
||||
fill=self.session.theme.neutral_color,
|
||||
font_size=1.2,
|
||||
|
||||
@@ -6,14 +6,16 @@ from rio import Component, Row, Text, TextStyle, Color
|
||||
class TournamentDetailsInfoRow(Component):
|
||||
key: str
|
||||
value: str
|
||||
color: Optional[Color] = None
|
||||
key_color: Optional[Color] = None
|
||||
value_color: Optional[Color] = None
|
||||
|
||||
|
||||
def build(self) -> Component:
|
||||
return Row(
|
||||
Text(
|
||||
text=self.key,
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
fill=self.key_color if self.key_color is not None else self.session.theme.background_color,
|
||||
font_size=1
|
||||
),
|
||||
margin_bottom=0.5,
|
||||
@@ -22,7 +24,7 @@ class TournamentDetailsInfoRow(Component):
|
||||
Text(
|
||||
text=self.value,
|
||||
style=TextStyle(
|
||||
fill=self.color if self.color is not None else self.session.theme.background_color,
|
||||
fill=self.value_color if self.value_color is not None else self.session.theme.background_color,
|
||||
font_size=1
|
||||
),
|
||||
margin_bottom=0.5,
|
||||
|
||||
@@ -9,8 +9,8 @@ from rio import Component, Column, Button, Color, TextStyle, Text, TextInput, Ro
|
||||
|
||||
from src.ezgg_lan_manager.services.UserService import UserService, NameNotAllowedError
|
||||
from src.ezgg_lan_manager.services.ConfigurationService import ConfigurationService
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
|
||||
class UserEditForm(Component):
|
||||
@@ -35,8 +35,13 @@ class UserEditForm(Component):
|
||||
async def on_populate(self) -> None:
|
||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Profil bearbeiten")
|
||||
if self.is_own_profile:
|
||||
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||
self.profile_picture = await self.session[UserService].get_profile_picture(self.user.user_id)
|
||||
try:
|
||||
user_id = self.session[UserSession].user_id
|
||||
except KeyError:
|
||||
self.session.navigate_to("/")
|
||||
else:
|
||||
self.user = await self.session[UserService].get_user(user_id)
|
||||
self.profile_picture = await self.session[UserService].get_profile_picture(self.user.user_id)
|
||||
else:
|
||||
self.profile_picture = await self.session[UserService].get_profile_picture(self.user.user_id)
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from rio import Component
|
||||
from src.ezgg_lan_manager.components.LoginBox import LoginBox
|
||||
from src.ezgg_lan_manager.components.UserInfoBox import UserInfoBox
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
class UserInfoAndLoginBox(Component):
|
||||
state_changed_cb: Callable
|
||||
def build(self) -> Component:
|
||||
if self.session[SessionStorage].user_id is None:
|
||||
return LoginBox(status_change_cb=self.force_refresh)
|
||||
else:
|
||||
return UserInfoBox(status_change_cb=self.force_refresh)
|
||||
try:
|
||||
user_id = self.session[UserSession].user_id
|
||||
return UserInfoBox(status_change_cb=self.state_changed_cb, user_id=user_id)
|
||||
except KeyError:
|
||||
return LoginBox(status_change_cb=self.state_changed_cb)
|
||||
|
||||
@@ -6,6 +6,7 @@ from rio import Component, TextStyle, Color, Button, Text, Rectangle, Column, Ro
|
||||
|
||||
from src.ezgg_lan_manager.components.UserInfoBoxButton import UserInfoBoxButton
|
||||
from src.ezgg_lan_manager.services.LocalDataService import LocalData, LocalDataService
|
||||
from src.ezgg_lan_manager.services.RefreshService import RefreshService
|
||||
from src.ezgg_lan_manager.services.UserService import UserService
|
||||
from src.ezgg_lan_manager.services.AccountingService import AccountingService
|
||||
from src.ezgg_lan_manager.services.TicketingService import TicketingService
|
||||
@@ -13,7 +14,7 @@ from src.ezgg_lan_manager.services.SeatingService import SeatingService
|
||||
from src.ezgg_lan_manager.types.Seat import Seat
|
||||
from src.ezgg_lan_manager.types.Ticket import Ticket
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
|
||||
class StatusButton(Component):
|
||||
@@ -41,6 +42,7 @@ class StatusButton(Component):
|
||||
|
||||
|
||||
class UserInfoBox(Component):
|
||||
user_id: int
|
||||
status_change_cb: EventHandler = None
|
||||
TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
|
||||
user: Optional[User] = None
|
||||
@@ -53,31 +55,28 @@ class UserInfoBox(Component):
|
||||
return choice(["Guten Tacho", "Tuten Gag", "Servus", "Moinjour", "Hallöchen", "Heyho", "Moinsen"])
|
||||
|
||||
async def logout(self) -> None:
|
||||
await self.session[SessionStorage].clear()
|
||||
self.session.detach(UserSession)
|
||||
self.user = None
|
||||
self.session[LocalDataService].del_session(self.session[LocalData].stored_session_token)
|
||||
self.session[LocalData].stored_session_token = None
|
||||
self.session.attach(self.session[LocalData])
|
||||
self.status_change_cb()
|
||||
self.session.navigate_to("/")
|
||||
if self.status_change_cb is not None:
|
||||
await self.status_change_cb()
|
||||
await self.session[RefreshService].trigger_refresh()
|
||||
self.session.navigate_to("")
|
||||
|
||||
@event.on_populate
|
||||
async def async_init(self) -> None:
|
||||
if self.session[SessionStorage].user_id:
|
||||
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||
self.user_balance = await self.session[AccountingService].get_balance(self.user.user_id)
|
||||
self.user_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id)
|
||||
self.user_seat = await self.session[SeatingService].get_user_seat(self.user.user_id)
|
||||
self.session[AccountingService].add_update_hook(self.update)
|
||||
|
||||
async def update(self) -> None:
|
||||
if not self.user:
|
||||
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||
if not self.user:
|
||||
return
|
||||
self.user = await self.session[UserService].get_user(self.user_id)
|
||||
self.user_balance = await self.session[AccountingService].get_balance(self.user.user_id)
|
||||
self.user_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id)
|
||||
self.user_seat = await self.session[SeatingService].get_user_seat(self.user.user_id)
|
||||
self.session[AccountingService].add_update_hook(self.update)
|
||||
|
||||
async def update(self) -> None:
|
||||
self.user_balance = await self.session[AccountingService].get_balance(self.user_id)
|
||||
self.user_ticket = await self.session[TicketingService].get_user_ticket(self.user_id)
|
||||
self.user_seat = await self.session[SeatingService].get_user_seat(self.user_id)
|
||||
|
||||
def build(self) -> Component:
|
||||
if not self.user:
|
||||
@@ -117,5 +116,5 @@ class UserInfoBox(Component):
|
||||
min_width=12,
|
||||
align_x=0.5,
|
||||
margin_top=0.3,
|
||||
margin_bottom=2
|
||||
margin_bottom=1.5
|
||||
)
|
||||
|
||||
@@ -3,22 +3,32 @@ from typing import Optional
|
||||
from rio import URL, GuardEvent
|
||||
|
||||
from src.ezgg_lan_manager.services.UserService import UserService
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
|
||||
# Guards pages against access from users that are NOT logged in
|
||||
def logged_in_guard(event: GuardEvent) -> Optional[URL]:
|
||||
if event.session[SessionStorage].user_id is None:
|
||||
try:
|
||||
_ = event.session[UserSession].user_id
|
||||
return None
|
||||
except KeyError:
|
||||
return URL("./")
|
||||
|
||||
# Guards pages against access from users that ARE logged in
|
||||
def not_logged_in_guard(event: GuardEvent) -> Optional[URL]:
|
||||
if event.session[SessionStorage].user_id is not None:
|
||||
try:
|
||||
_ = event.session[UserSession].user_id
|
||||
return URL("./")
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
# Guards pages against access from users that are NOT logged in and NOT team members
|
||||
def team_guard(event: GuardEvent) -> Optional[URL]:
|
||||
user_id = event.session[SessionStorage].user_id
|
||||
is_team_member = event.session[SessionStorage].is_team_member
|
||||
if user_id is None or not is_team_member:
|
||||
try:
|
||||
user_id = event.session[UserSession].user_id
|
||||
is_team_member = event.session[UserSession].is_team_member
|
||||
if user_id and is_team_member:
|
||||
return None
|
||||
return URL("./")
|
||||
except KeyError:
|
||||
return URL("./")
|
||||
|
||||
@@ -1,29 +1,39 @@
|
||||
from decimal import Decimal
|
||||
from functools import partial
|
||||
from typing import Optional
|
||||
|
||||
from rio import Column, Component, event, Text, TextStyle, Button, Color, Revealer, Row, ProgressCircle, Link
|
||||
from rio import Column, Component, event, Text, TextStyle, Button, Color, Revealer, Row, ProgressCircle, Link, Image
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService, UserService, AccountingService
|
||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.Transaction import Transaction
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
|
||||
class AccountPage(Component):
|
||||
user: Optional[User] = None
|
||||
balance: Optional[Decimal] = None
|
||||
transaction_history: list[Transaction] = list()
|
||||
payment_qr_image: bytes = None
|
||||
banking_info_revealer_open: bool = False
|
||||
paypal_info_revealer_open: bool = False
|
||||
|
||||
@event.on_populate
|
||||
async def on_populate(self) -> None:
|
||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Guthabenkonto")
|
||||
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||
self.balance = await self.session[AccountingService].get_balance(self.user.user_id)
|
||||
self.transaction_history = await self.session[AccountingService].get_transaction_history(self.user.user_id)
|
||||
try:
|
||||
user_id = self.session[UserSession].user_id
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self.user = await self.session[UserService].get_user(user_id)
|
||||
self.balance = await self.session[AccountingService].get_balance(user_id)
|
||||
self.transaction_history = await self.session[AccountingService].get_transaction_history(user_id)
|
||||
self.payment_qr_image = self.session[AccountingService].make_payment_qr_image(
|
||||
"Einfach Zocken Gaming Gesellschaft",
|
||||
"GENODE51BIK",
|
||||
"DE47517624340019856607",
|
||||
f"AUFLADUNG - {self.user.user_id} - {self.user.user_name}")
|
||||
|
||||
async def _on_banking_info_press(self) -> None:
|
||||
self.banking_info_revealer_open = not self.banking_info_revealer_open
|
||||
@@ -32,7 +42,7 @@ class AccountPage(Component):
|
||||
self.paypal_info_revealer_open = not self.paypal_info_revealer_open
|
||||
|
||||
def build(self) -> Component:
|
||||
if not self.user and not self.balance:
|
||||
if not self.user or not self.payment_qr_image:
|
||||
return Column(
|
||||
MainViewContentBox(
|
||||
ProgressCircle(
|
||||
@@ -81,6 +91,10 @@ class AccountPage(Component):
|
||||
margin=0,
|
||||
margin_bottom=1,
|
||||
align_x=0.5
|
||||
),
|
||||
Image(self.payment_qr_image,
|
||||
min_width=20,
|
||||
min_height=20
|
||||
)
|
||||
),
|
||||
margin=2,
|
||||
@@ -219,19 +233,20 @@ class AccountPage(Component):
|
||||
on_press=self._on_paypal_info_press
|
||||
),
|
||||
paypal_info_revealer,
|
||||
Link(
|
||||
content=Button(
|
||||
content=Text("PAYPAL (3% Gebühr - Gewerblich)", style=TextStyle(fill=Color.from_hex("121212"), font_size=0.8), justify="center"),
|
||||
shape="rectangle",
|
||||
style="major",
|
||||
color="secondary",
|
||||
grow_x=True,
|
||||
margin=2,
|
||||
margin_top=0
|
||||
),
|
||||
target_url="https://www.paypal.com/ncp/payment/89YMGVZ4S33RS",
|
||||
open_in_new_tab=True
|
||||
)
|
||||
# Disabled because people did not understand the fee's and kept charging 24.03 € to their accounts
|
||||
# Link(
|
||||
# content=Button(
|
||||
# content=Text("PAYPAL (3% Gebühr - Gewerblich)", style=TextStyle(fill=Color.from_hex("121212"), font_size=0.8), justify="center"),
|
||||
# shape="rectangle",
|
||||
# style="major",
|
||||
# color="secondary",
|
||||
# grow_x=True,
|
||||
# margin=2,
|
||||
# margin_top=0
|
||||
# ),
|
||||
# target_url="https://www.paypal.com/ncp/payment/89YMGVZ4S33RS",
|
||||
# open_in_new_tab=True
|
||||
# )
|
||||
)
|
||||
),
|
||||
MainViewContentBox(
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
from rio import Column, Component, event, Text, TextStyle, Row
|
||||
|
||||
from src.ezgg_lan_manager.components.AdminNavigationCard import AdminNavigationCard
|
||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||
from src.ezgg_lan_manager.services.ConfigurationService import ConfigurationService
|
||||
|
||||
|
||||
class AdminNavigationPage(Component):
|
||||
@event.on_populate
|
||||
async def on_populate(self) -> None:
|
||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Admin")
|
||||
|
||||
def build(self) -> Component:
|
||||
return Column(
|
||||
MainViewContentBox(
|
||||
Text(
|
||||
text="Admin",
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
margin_top=2,
|
||||
margin_bottom=2,
|
||||
align_x=0.5
|
||||
)
|
||||
),
|
||||
MainViewContentBox(
|
||||
Column(
|
||||
Row(
|
||||
AdminNavigationCard(icon_name="material/supervised_user_circle", display_text="Nutzer", target_url="manage-users"),
|
||||
AdminNavigationCard(icon_name="material/fastfood", display_text="Catering", target_url="manage-catering"),
|
||||
spacing=1
|
||||
),
|
||||
Row(
|
||||
AdminNavigationCard(icon_name="material/text_ad", display_text="News", target_url="manage-news"),
|
||||
AdminNavigationCard(icon_name="material/trophy", display_text="Turniere", target_url="manage-tournaments"),
|
||||
spacing=1
|
||||
),
|
||||
margin=1,
|
||||
spacing=1
|
||||
)
|
||||
),
|
||||
align_y=0
|
||||
)
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import * # type: ignore
|
||||
|
||||
from rio import Component, event, Spacer, Card, Container, Column, Row, TextStyle, Color, Text, PageView, Button
|
||||
from rio import Component, event, Spacer, Card, Container, Column, Row, TextStyle, Color, Text, PageView, Button, Link
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService, DatabaseService
|
||||
from src.ezgg_lan_manager.components.DesktopNavigation import DesktopNavigation
|
||||
@@ -58,7 +58,15 @@ class BasePage(Component):
|
||||
Row(
|
||||
Spacer(grow_x=True, grow_y=False),
|
||||
Card(
|
||||
content=Text(f"EZGG LAN Manager Version {self.session[ConfigurationService].APP_VERSION} © EZ GG e.V.", align_x=0.5, align_y=0.5, fill=self.session.theme.primary_color, style=TextStyle(font_size=0.5)),
|
||||
content=Row(
|
||||
Text(f"EZGG LAN Manager Version {self.session[ConfigurationService].APP_VERSION} © EZ GG e.V.", fill=self.session.theme.primary_color, style=TextStyle(font_size=0.5)),
|
||||
Text(f"-", fill=self.session.theme.primary_color, style=TextStyle(font_size=0.5), margin_left=0.5, margin_right=0.5),
|
||||
Link(content=Text(f"Impressum & DSGVO", fill=self.session.theme.primary_color, style=TextStyle(font_size=0.5)), target_url="./imprint"),
|
||||
Text(f"-", fill=self.session.theme.primary_color, style=TextStyle(font_size=0.5), margin_left=0.5, margin_right=0.5),
|
||||
Link(content=Text(f"Kontakt", fill=self.session.theme.primary_color, style=TextStyle(font_size=0.5)), target_url="./contact"),
|
||||
align_x=0.5,
|
||||
align_y=0.5
|
||||
),
|
||||
color=self.session.theme.neutral_color,
|
||||
corner_radius=(0, 0, 0.5, 0.5),
|
||||
grow_x=False,
|
||||
|
||||
@@ -2,14 +2,14 @@ from typing import Optional
|
||||
|
||||
from rio import Text, Column, TextStyle, Component, event, Button, Popup
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService, UserService, TicketingService
|
||||
from src.ezgg_lan_manager import ConfigurationService, UserService, TicketingService, RefreshService
|
||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||
from src.ezgg_lan_manager.components.TicketBuyCard import TicketBuyCard
|
||||
from src.ezgg_lan_manager.services.AccountingService import InsufficientFundsError
|
||||
from src.ezgg_lan_manager.services.TicketingService import TicketNotAvailableError, UserAlreadyHasTicketError
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.Ticket import Ticket
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
|
||||
class BuyTicketPage(Component):
|
||||
@@ -19,14 +19,24 @@ class BuyTicketPage(Component):
|
||||
popup_message: str = ""
|
||||
is_popup_success: bool = False
|
||||
is_buying_enabled: bool = False
|
||||
is_user_logged_in: bool = False
|
||||
|
||||
@event.on_populate
|
||||
async def on_populate(self) -> None:
|
||||
self.session[RefreshService].subscribe(self.on_populate)
|
||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Ticket kaufen")
|
||||
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||
try:
|
||||
user_id = self.session[UserSession].user_id
|
||||
except KeyError:
|
||||
self.user = None
|
||||
else:
|
||||
self.user = await self.session[UserService].get_user(user_id)
|
||||
if self.user is None: # No user logged in
|
||||
self.is_buying_enabled = False
|
||||
self.is_user_logged_in = False
|
||||
self.user_ticket = None
|
||||
else: # User is logged in
|
||||
self.is_user_logged_in = True
|
||||
possible_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id)
|
||||
self.user_ticket = possible_ticket
|
||||
if possible_ticket is not None: # User already has a ticket
|
||||
@@ -67,17 +77,29 @@ class BuyTicketPage(Component):
|
||||
|
||||
def build(self) -> Component:
|
||||
ticket_infos = self.session[ConfigurationService].get_ticket_info()
|
||||
header = Text(
|
||||
"Tickets & Preise",
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
header = Column(
|
||||
Text(
|
||||
"Tickets & Preise",
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
margin_top=2,
|
||||
align_x=0.5
|
||||
),
|
||||
margin_top=2,
|
||||
margin_bottom=2,
|
||||
align_x=0.5
|
||||
spacing=0.2
|
||||
)
|
||||
|
||||
if not self.is_user_logged_in:
|
||||
header.add(Text(
|
||||
"Du musst eingeloggt sein\num ein Ticket zu kaufen",
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=0.6
|
||||
),
|
||||
align_x=0.5
|
||||
))
|
||||
|
||||
return Column(
|
||||
MainViewContentBox(
|
||||
Column(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Optional, Callable
|
||||
from typing import Optional
|
||||
|
||||
from rio import Column, Component, event, TextStyle, Text, Spacer, Revealer, SwitcherBar, SwitcherBarChangeEvent, ProgressCircle
|
||||
|
||||
@@ -6,8 +6,9 @@ from src.ezgg_lan_manager import ConfigurationService, CateringService
|
||||
from src.ezgg_lan_manager.components.CateringSelectionItem import CateringSelectionItem
|
||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||
from src.ezgg_lan_manager.components.ShoppingCartAndOrders import ShoppingCartAndOrders
|
||||
from src.ezgg_lan_manager.services.RefreshService import RefreshService
|
||||
from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItemCategory, CateringMenuItem
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
|
||||
class CateringPage(Component):
|
||||
@@ -15,11 +16,9 @@ class CateringPage(Component):
|
||||
all_menu_items: Optional[list[CateringMenuItem]] = None
|
||||
shopping_cart_and_orders: list[ShoppingCartAndOrders] = []
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.session[SessionStorage].subscribe_to_logged_in_or_out_event(self.__class__.__name__, self.on_user_logged_in_status_changed)
|
||||
|
||||
@event.on_populate
|
||||
async def on_populate(self) -> None:
|
||||
self.session[RefreshService].subscribe(self.on_populate)
|
||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Catering")
|
||||
self.all_menu_items = await self.session[CateringService].get_menu()
|
||||
|
||||
@@ -34,7 +33,10 @@ class CateringPage(Component):
|
||||
return list(filter(lambda item: item.category == category, all_menu_items))
|
||||
|
||||
def build(self) -> Component:
|
||||
user_id = self.session[SessionStorage].user_id
|
||||
try:
|
||||
user_id = self.session[UserSession].user_id
|
||||
except KeyError:
|
||||
user_id = None
|
||||
if len(self.shopping_cart_and_orders) == 0:
|
||||
self.shopping_cart_and_orders.append(ShoppingCartAndOrders())
|
||||
if len(self.shopping_cart_and_orders) > 1:
|
||||
|
||||
@@ -5,8 +5,8 @@ from rio import Text, Column, TextStyle, Component, event, TextInput, MultiLineT
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService, UserService, MailingService
|
||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
|
||||
class ContactPage(Component):
|
||||
@@ -25,9 +25,9 @@ class ContactPage(Component):
|
||||
@event.on_populate
|
||||
async def on_populate(self) -> None:
|
||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Kontakt")
|
||||
if self.session[SessionStorage].user_id is not None:
|
||||
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||
else:
|
||||
try:
|
||||
self.user = await self.session[UserService].get_user(self.session[UserSession].user_id)
|
||||
except KeyError:
|
||||
self.user = None
|
||||
self.e_mail = "" if not self.user else self.user.user_mail
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from random import randint
|
||||
from typing import * # type: ignore
|
||||
|
||||
from rio import Component, event, Column, Row, Color, Rectangle
|
||||
|
||||
|
||||
|
||||
class ConwayPage(Component):
|
||||
"""
|
||||
This is an Easter egg.
|
||||
"""
|
||||
|
||||
active_generation: list[list] = []
|
||||
rows: int = 36
|
||||
cols: int = 20
|
||||
|
||||
@event.periodic(1)
|
||||
async def calc_next_gen(self) -> None:
|
||||
self.create_next_grid()
|
||||
|
||||
@event.on_populate
|
||||
def prepare(self) -> None:
|
||||
self.active_generation = self.create_initial_grid()
|
||||
|
||||
def create_initial_grid(self) -> list[list]:
|
||||
grid = []
|
||||
for row in range(self.rows):
|
||||
grid_rows = []
|
||||
for col in range(self.cols):
|
||||
if randint(0, 7) == 0:
|
||||
grid_rows += [1]
|
||||
else:
|
||||
grid_rows += [0]
|
||||
grid += [grid_rows]
|
||||
return grid
|
||||
|
||||
def create_next_grid(self) -> None:
|
||||
next_grid = deepcopy(self.active_generation)
|
||||
|
||||
for row in range(self.rows):
|
||||
for col in range(self.cols):
|
||||
live_neighbors = self.get_live_neighbors(row, col, self.active_generation)
|
||||
|
||||
if live_neighbors < 2 or live_neighbors > 3:
|
||||
next_grid[row][col] = 0
|
||||
elif live_neighbors == 3 and self.active_generation[row][col] == 0:
|
||||
next_grid[row][col] = 1
|
||||
else:
|
||||
next_grid[row][col] = self.active_generation[row][col]
|
||||
|
||||
self.active_generation = next_grid
|
||||
|
||||
def get_live_neighbors(self, row: int, col: int, grid: list[list]) -> int:
|
||||
life_sum = 0
|
||||
for i in range(-1, 2):
|
||||
for j in range(-1, 2):
|
||||
if not (i == 0 and j == 0):
|
||||
life_sum += grid[((row + i) % self.rows)][((col + j) % self.cols)]
|
||||
return life_sum
|
||||
|
||||
def grid_changing(self, next_grid: list[list]) -> bool:
|
||||
for row in range(self.rows):
|
||||
for col in range(self.cols):
|
||||
if not self.active_generation[row][col] == next_grid[row][col]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def build(self) -> Component:
|
||||
rows = []
|
||||
|
||||
for row in self.active_generation:
|
||||
rectangles = []
|
||||
|
||||
for cell in row:
|
||||
color = Color.WHITE if cell == 1 else Color.BLACK
|
||||
rectangles.append(Rectangle(fill=color, transition_time=0.3))
|
||||
|
||||
rows.append(Row(*rectangles))
|
||||
|
||||
return Column(*rows)
|
||||
@@ -1,23 +1,14 @@
|
||||
from typing import Optional
|
||||
|
||||
from rio import Column, Component, event, Spacer
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService, UserService
|
||||
from src.ezgg_lan_manager import ConfigurationService
|
||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||
from src.ezgg_lan_manager.components.UserEditForm import UserEditForm
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
|
||||
class EditProfilePage(Component):
|
||||
user: Optional[User] = None
|
||||
pfp: Optional[bytes] = None
|
||||
|
||||
@event.on_populate
|
||||
async def on_populate(self) -> None:
|
||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Profil bearbeiten")
|
||||
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||
self.pfp = await self.session[UserService].get_profile_picture(self.user.user_id)
|
||||
|
||||
def build(self) -> Component:
|
||||
return Column(
|
||||
|
||||
@@ -27,7 +27,7 @@ class ForgotPasswordPage(Component):
|
||||
user = await user_service.get_user(self.email_input.text.strip())
|
||||
if user is not None:
|
||||
new_password = "".join(choices(user_service.ALLOWED_USER_NAME_SYMBOLS, k=16))
|
||||
user.user_password = sha256(new_password.encode(encoding="utf-8")).hexdigest()
|
||||
user.user_fallback_password = sha256(new_password.encode(encoding="utf-8")).hexdigest()
|
||||
await user_service.update_user(user)
|
||||
await mailing_service.send_email(
|
||||
subject=f"Dein neues Passwort für {lan_info.name}",
|
||||
|
||||
@@ -1,32 +1,121 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from typing import Optional
|
||||
|
||||
from rio import Column, Component, event, TextStyle, Text, Spacer
|
||||
from from_root import from_root
|
||||
from rio import Column, Component, event, TextStyle, Text, Spacer, Row, Image, Tooltip, IconButton, Popup, Rectangle, Dropdown, ThemeContextSwitcher, Button
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService
|
||||
from src.ezgg_lan_manager import ConfigurationService, TournamentService, UserService
|
||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||
from src.ezgg_lan_manager.types.DateUtil import weekday_to_display_text
|
||||
from src.ezgg_lan_manager.types.Participant import Participant
|
||||
from src.ezgg_lan_manager.types.Tournament import Tournament
|
||||
from src.ezgg_lan_manager.types.TournamentBase import TournamentStatus
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
class ManageTournamentsPage(Component):
|
||||
tournaments: list[Tournament] = []
|
||||
remove_participant_popup_open: bool = False
|
||||
cancel_options: dict[str, Optional[Participant]] = {"": None}
|
||||
tournament_id_selected_for_participant_removal: Optional[int] = None
|
||||
participant_selected_for_removal: Optional[Participant] = None
|
||||
|
||||
@event.on_populate
|
||||
async def on_populate(self) -> None:
|
||||
self.tournaments = await self.session[TournamentService].get_tournaments()
|
||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Turnier Verwaltung")
|
||||
|
||||
async def on_start_pressed(self, tournament_id: int) -> None:
|
||||
logger.info(f"Starting tournament with ID {tournament_id}")
|
||||
await self.session[TournamentService].start_tournament(tournament_id)
|
||||
|
||||
async def on_cancel_pressed(self, tournament_id: int) -> None:
|
||||
logger.info(f"Canceling tournament with ID {tournament_id}")
|
||||
await self.session[TournamentService].cancel_tournament(tournament_id)
|
||||
|
||||
async def on_remove_participant_pressed(self, tournament_id: int) -> None:
|
||||
tournament = await self.session[TournamentService].get_tournament_by_id(tournament_id)
|
||||
if tournament is None:
|
||||
return
|
||||
users = await self.session[UserService].get_all_users()
|
||||
try:
|
||||
cancel_options = {next(filter(lambda u: u.user_id == p.id, users)).user_name: p for p in tournament.participants}
|
||||
if cancel_options:
|
||||
self.cancel_options = cancel_options
|
||||
else:
|
||||
self.cancel_options = {"": None}
|
||||
except StopIteration as e:
|
||||
logger.error(f"Error trying to find user for participant: {e}")
|
||||
self.tournament_id_selected_for_participant_removal = tournament_id
|
||||
self.remove_participant_popup_open = True
|
||||
|
||||
async def on_remove_participant_confirm_pressed(self) -> None:
|
||||
if self.participant_selected_for_removal is not None and self.tournament_id_selected_for_participant_removal is not None:
|
||||
logger.info(f"Removing participant with ID {self.participant_selected_for_removal.id} from tournament with ID {self.tournament_id_selected_for_participant_removal}")
|
||||
await self.session[TournamentService].unregister_user_from_tournament(self.participant_selected_for_removal.id, self.tournament_id_selected_for_participant_removal)
|
||||
await self.on_remove_participant_cancel_pressed()
|
||||
|
||||
async def on_remove_participant_cancel_pressed(self) -> None:
|
||||
self.tournament_id_selected_for_participant_removal = None
|
||||
self.participant_selected_for_removal = None
|
||||
self.remove_participant_popup_open = False
|
||||
|
||||
def build(self) -> Component:
|
||||
return Column(
|
||||
MainViewContentBox(
|
||||
Column(
|
||||
Text(
|
||||
text="Turnier Verwaltung",
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
margin_top=2,
|
||||
margin_bottom=2,
|
||||
align_x=0.5
|
||||
)
|
||||
tournament_rows = []
|
||||
for tournament in self.tournaments:
|
||||
start_time_color = self.session.theme.background_color
|
||||
if tournament.start_time < datetime.now() and tournament.status == TournamentStatus.OPEN:
|
||||
start_time_color = self.session.theme.warning_color
|
||||
|
||||
tournament_rows.append(
|
||||
Row(
|
||||
Image(image=from_root(f"src/ezgg_lan_manager/assets/img/games/{tournament.game_title.image_name}"), min_width=1.5, margin_right=1),
|
||||
Text(tournament.name, style=TextStyle(fill=self.session.theme.background_color, font_size=0.8), justify="left", margin_right=1.5),
|
||||
Text(f"{weekday_to_display_text(tournament.start_time.weekday())[:2]}.{tournament.start_time.strftime('%H:%M')} Uhr", style=TextStyle(fill=start_time_color, font_size=0.8), justify="left", margin_right=1),
|
||||
Spacer(),
|
||||
Tooltip(anchor=IconButton("material/play_arrow", min_size=2, margin_right=0.5, on_press=partial(self.on_start_pressed, tournament.id)), tip="Starten"),
|
||||
Tooltip(anchor=IconButton("material/cancel_schedule_send", min_size=2, margin_right=0.5, on_press=partial(self.on_cancel_pressed, tournament.id)), tip="Absagen"),
|
||||
Tooltip(anchor=IconButton("material/person_cancel", min_size=2, on_press=partial(self.on_remove_participant_pressed, tournament.id)), tip="Spieler entfernen"),
|
||||
margin=1
|
||||
)
|
||||
)
|
||||
return Column(
|
||||
Popup(
|
||||
anchor=MainViewContentBox(
|
||||
Column(
|
||||
Text(
|
||||
text="Turnier Verwaltung",
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
margin_top=2,
|
||||
margin_bottom=2,
|
||||
align_x=0.5
|
||||
),
|
||||
*tournament_rows
|
||||
)
|
||||
),
|
||||
content=Rectangle(
|
||||
content=Row(
|
||||
ThemeContextSwitcher(
|
||||
content=Dropdown(options=self.cancel_options, min_width=20, selected_value=self.bind().participant_selected_for_removal), color=self.session.theme.hud_color
|
||||
),
|
||||
Button(content="REMOVE", shape="rectangle", grow_x=False, on_press=self.on_remove_participant_confirm_pressed),
|
||||
Button(content="CANCEL", shape="rectangle", grow_x=False, on_press=self.on_remove_participant_cancel_pressed),
|
||||
margin=0.5
|
||||
),
|
||||
min_width=30,
|
||||
min_height=4,
|
||||
fill=self.session.theme.primary_color,
|
||||
margin_top=3.5,
|
||||
stroke_width=0.3,
|
||||
stroke_color=self.session.theme.neutral_color,
|
||||
),
|
||||
is_open=self.remove_participant_popup_open,
|
||||
color="none"
|
||||
),
|
||||
Spacer()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ from dataclasses import field
|
||||
from typing import Optional
|
||||
|
||||
from rio import Column, Component, event, TextStyle, Text, TextInput, ThemeContextSwitcher, Grid, \
|
||||
PointerEventListener, PointerEvent, Rectangle, CursorStyle, Color, TextInputChangeEvent, Spacer, Row, Switch, \
|
||||
PointerEventListener, PointerEvent, Rectangle, Color, TextInputChangeEvent, Spacer, Row, Switch, \
|
||||
SwitchChangeEvent, EventHandler
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService, UserService, AccountingService, SeatingService, MailingService
|
||||
@@ -11,9 +11,9 @@ from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBo
|
||||
from src.ezgg_lan_manager.components.NewTransactionForm import NewTransactionForm
|
||||
from src.ezgg_lan_manager.components.UserEditForm import UserEditForm
|
||||
from src.ezgg_lan_manager.services.AccountingService import InsufficientFundsError
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.Transaction import Transaction
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
@@ -42,7 +42,7 @@ class ClickableGridContent(Component):
|
||||
grow_x=True
|
||||
),
|
||||
fill=Color.TRANSPARENT,
|
||||
cursor=CursorStyle.POINTER
|
||||
cursor="pointer"
|
||||
),
|
||||
on_pointer_enter=self.on_mouse_enter,
|
||||
on_pointer_leave=self.on_mouse_leave,
|
||||
@@ -84,7 +84,11 @@ class ManageUsersPage(Component):
|
||||
await self.session[UserService].update_user(self.selected_user)
|
||||
|
||||
async def on_new_transaction(self, transaction: Transaction) -> None:
|
||||
if not self.session[SessionStorage].is_team_member: # Better safe than sorry
|
||||
try:
|
||||
user = await self.session[UserService].get_user(self.session[UserSession].user_id)
|
||||
if not user.is_team_member: # Better safe than sorry
|
||||
return
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
logger.info(f"Got new transaction for user with ID '{transaction.user_id}' over "
|
||||
|
||||
@@ -75,7 +75,7 @@ class OverviewPage(Component):
|
||||
Row(
|
||||
Text("Internet", fill=self.session.theme.neutral_color, margin_left=1),
|
||||
Spacer(),
|
||||
Text(f"60/20 Mbit/s (down/up)", fill=self.session.theme.neutral_color, margin_right=1),
|
||||
Text(f"100/50 Mbit/s (down/up)", fill=self.session.theme.neutral_color, margin_right=1),
|
||||
margin_bottom=0.3
|
||||
),
|
||||
Row(
|
||||
@@ -125,17 +125,5 @@ class OverviewPage(Component):
|
||||
)
|
||||
)
|
||||
),
|
||||
MainViewContentBox(
|
||||
Column(
|
||||
Text("Turniere & Ablauf", font_size=2, justify="center", fill=self.session.theme.neutral_color, margin_top=0.5, margin_bottom=1),
|
||||
Column(
|
||||
Row(
|
||||
Text("Zum aktuellen Zeitpunkt steht noch nicht fest welche Turniere gespielt werden. Wir planen diverse Online- und Offline Turniere mit Preisen durchzuführen. Weitere Informationen gibt es, sobald sie kommen, auf der NEWS- und Turnier-Seite.", font_size=0.7,
|
||||
fill=self.session.theme.neutral_color, margin_left=1, overflow="wrap"),
|
||||
margin_bottom=0.3
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Spacer()
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import logging
|
||||
from asyncio import sleep, create_task
|
||||
|
||||
from email_validator import validate_email, EmailNotValidError
|
||||
from rio import Column, Component, event, Text, TextStyle, TextInput, TextInputChangeEvent, Button
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService, UserService, MailingService
|
||||
from src.ezgg_lan_manager.components.AnimatedText import AnimatedText
|
||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||
|
||||
MINIMUM_PASSWORD_LENGTH = 6
|
||||
@@ -13,125 +13,154 @@ logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
|
||||
class RegisterPage(Component):
|
||||
pw_1: str = ""
|
||||
pw_2: str = ""
|
||||
email: str = ""
|
||||
user_name: str = ""
|
||||
pw_1_valid: bool = True
|
||||
pw_2_valid: bool = True
|
||||
email_valid: bool = True
|
||||
submit_button_loading: bool = False
|
||||
display_text: str = ""
|
||||
display_text_style: TextStyle = TextStyle()
|
||||
|
||||
def on_pw_focus_loss(self, _: TextInputChangeEvent) -> None:
|
||||
if not (self.pw_1.text == self.pw_2.text) or len(self.pw_1.text) < MINIMUM_PASSWORD_LENGTH:
|
||||
self.pw_1.is_valid = False
|
||||
self.pw_2.is_valid = False
|
||||
if not (self.pw_1 == self.pw_2) or len(self.pw_1) < MINIMUM_PASSWORD_LENGTH:
|
||||
self.pw_1_valid = False
|
||||
self.pw_2_valid = False
|
||||
return
|
||||
self.pw_1.is_valid = True
|
||||
self.pw_2.is_valid = True
|
||||
self.pw_1_valid = True
|
||||
self.pw_2_valid = True
|
||||
|
||||
def on_email_focus_loss(self, change_event: TextInputChangeEvent) -> None:
|
||||
try:
|
||||
validate_email(change_event.text, check_deliverability=False)
|
||||
self.email_input.is_valid = True
|
||||
self.email_valid = True
|
||||
except EmailNotValidError:
|
||||
self.email_input.is_valid = False
|
||||
self.email_valid = False
|
||||
|
||||
def on_user_name_focus_loss(self, _: TextInputChangeEvent) -> None:
|
||||
current_text = self.user_name_input.text
|
||||
current_text = self.user_name
|
||||
if len(current_text) > UserService.MAX_USERNAME_LENGTH:
|
||||
self.user_name_input.text = current_text[:UserService.MAX_USERNAME_LENGTH]
|
||||
self.user_name = current_text[:UserService.MAX_USERNAME_LENGTH]
|
||||
|
||||
async def on_submit_button_pressed(self) -> None:
|
||||
self.submit_button.is_loading = True
|
||||
self.submit_button.force_refresh()
|
||||
self.submit_button_loading = True
|
||||
|
||||
if len(self.user_name_input.text) < 1:
|
||||
await self.animated_text.display_text(False, "Nutzername darf nicht leer sein!")
|
||||
self.submit_button.is_loading = False
|
||||
if len(self.user_name) < 1:
|
||||
await self.display_animated_text(False, "Nutzername darf nicht leer sein!")
|
||||
self.submit_button_loading = False
|
||||
return
|
||||
|
||||
if not (self.pw_1.text == self.pw_2.text):
|
||||
await self.animated_text.display_text(False, "Passwörter stimmen nicht überein!")
|
||||
self.submit_button.is_loading = False
|
||||
if not (self.pw_1 == self.pw_2):
|
||||
await self.display_animated_text(False, "Passwörter stimmen nicht überein!")
|
||||
self.submit_button_loading = False
|
||||
return
|
||||
|
||||
if len(self.pw_1.text) < MINIMUM_PASSWORD_LENGTH:
|
||||
await self.animated_text.display_text(False, f"Passwort muss mindestens {MINIMUM_PASSWORD_LENGTH} Zeichen lang sein!")
|
||||
self.submit_button.is_loading = False
|
||||
if len(self.pw_1) < MINIMUM_PASSWORD_LENGTH:
|
||||
await self.display_animated_text(False, f"Passwort muss mindestens {MINIMUM_PASSWORD_LENGTH} Zeichen lang sein!")
|
||||
self.submit_button_loading = False
|
||||
return
|
||||
|
||||
if not self.email_input.is_valid or len(self.email_input.text) < 3:
|
||||
await self.animated_text.display_text(False, "E-Mail Adresse ungültig!")
|
||||
self.submit_button.is_loading = False
|
||||
if not self.email_valid or len(self.email) < 3:
|
||||
await self.display_animated_text(False, "E-Mail Adresse ungültig!")
|
||||
self.submit_button_loading = False
|
||||
return
|
||||
|
||||
user_service = self.session[UserService]
|
||||
mailing_service = self.session[MailingService]
|
||||
lan_info = self.session[ConfigurationService].get_lan_info()
|
||||
|
||||
if await user_service.get_user(self.email_input.text) is not None or await user_service.get_user(self.user_name_input.text) is not None:
|
||||
await self.animated_text.display_text(False, "Benutzername oder E-Mail bereits regestriert!")
|
||||
self.submit_button.is_loading = False
|
||||
if await user_service.get_user(self.email) is not None or await user_service.get_user(self.user_name) is not None:
|
||||
await self.display_animated_text(False, "Benutzername oder E-Mail bereits registriert!")
|
||||
self.submit_button_loading = False
|
||||
return
|
||||
|
||||
try:
|
||||
new_user = await user_service.create_user(self.user_name_input.text, self.email_input.text, self.pw_1.text)
|
||||
new_user = await user_service.create_user(self.user_name, self.email, self.pw_1)
|
||||
if not new_user:
|
||||
logger.warning(f"UserService.create_user returned: {new_user}") # ToDo: Seems like the user is created fine, even if not returned #FixMe
|
||||
logger.error(f"create_user returned: {new_user}")
|
||||
raise Exception(f"create_user returned: {new_user}")
|
||||
except Exception as e:
|
||||
logger.error(f"Unknown error during new user registration: {e}")
|
||||
await self.animated_text.display_text(False, "Es ist ein unbekannter Fehler aufgetreten :(")
|
||||
self.submit_button.is_loading = False
|
||||
await self.display_animated_text(False, "Es ist ein unbekannter Fehler aufgetreten :(")
|
||||
self.submit_button_loading = False
|
||||
return
|
||||
|
||||
await mailing_service.send_email(
|
||||
subject="Erfolgreiche Registrierung",
|
||||
body=f"Hallo {self.user_name_input.text},\n\n"
|
||||
body=f"Hallo {self.user_name},\n\n"
|
||||
f"Du hast dich erfolgreich beim EZGG-LAN Manager für {lan_info.name} {lan_info.iteration} registriert.\n\n"
|
||||
f"Wenn du dich nicht registriert hast, kontaktiere bitte unser Team über unsere Homepage.\n\n"
|
||||
f"Liebe Grüße\nDein {lan_info.name} - Team",
|
||||
receiver=self.email_input.text
|
||||
receiver=self.email
|
||||
)
|
||||
|
||||
self.submit_button.is_loading = False
|
||||
await self.animated_text.display_text(True, "Erfolgreich registriert!")
|
||||
self.submit_button_loading = False
|
||||
await self.display_animated_text(True, "Erfolgreich registriert!")
|
||||
|
||||
@event.on_populate
|
||||
async def on_populate(self) -> None:
|
||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Registrieren")
|
||||
|
||||
async def display_animated_text(self, success: bool, text: str) -> None:
|
||||
self.display_text = ""
|
||||
style = TextStyle(
|
||||
fill=self.session.theme.success_color if success else self.session.theme.danger_color,
|
||||
font_size=0.9
|
||||
)
|
||||
|
||||
self.display_text_style = style
|
||||
_ = create_task(self._animate_text(text))
|
||||
|
||||
async def _animate_text(self, text: str) -> None:
|
||||
for c in text:
|
||||
self.display_text += c
|
||||
await sleep(0.06)
|
||||
|
||||
def build(self) -> Component:
|
||||
self.user_name_input = TextInput(
|
||||
user_name_input = TextInput(
|
||||
label="Benutzername",
|
||||
text="",
|
||||
text=self.bind().user_name,
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
margin_bottom=1,
|
||||
grow_x=True,
|
||||
on_lose_focus=self.on_user_name_focus_loss
|
||||
)
|
||||
self.email_input = TextInput(
|
||||
email_input = TextInput(
|
||||
label="E-Mail Adresse",
|
||||
text="",
|
||||
text=self.bind().email,
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
margin_bottom=1,
|
||||
grow_x=True,
|
||||
on_lose_focus=self.on_email_focus_loss
|
||||
on_lose_focus=self.on_email_focus_loss,
|
||||
is_valid=self.email_valid
|
||||
)
|
||||
self.pw_1 = TextInput(
|
||||
pw_1_input = TextInput(
|
||||
label="Passwort",
|
||||
text="",
|
||||
text=self.bind().pw_1,
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
margin_bottom=1,
|
||||
grow_x=True,
|
||||
is_secret=True,
|
||||
on_lose_focus=self.on_pw_focus_loss
|
||||
on_lose_focus=self.on_pw_focus_loss,
|
||||
is_valid=self.pw_1_valid
|
||||
)
|
||||
self.pw_2 = TextInput(
|
||||
pw_2_input = TextInput(
|
||||
label="Passwort wiederholen",
|
||||
text="",
|
||||
text=self.bind().pw_2,
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
margin_bottom=1,
|
||||
grow_x=True,
|
||||
is_secret=True,
|
||||
on_lose_focus=self.on_pw_focus_loss
|
||||
on_lose_focus=self.on_pw_focus_loss,
|
||||
is_valid=self.pw_2_valid
|
||||
)
|
||||
self.submit_button = Button(
|
||||
submit_button = Button(
|
||||
content=Text(
|
||||
"Registrieren",
|
||||
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9),
|
||||
@@ -145,13 +174,8 @@ class RegisterPage(Component):
|
||||
shape="rectangle",
|
||||
style="minor",
|
||||
color=self.session.theme.secondary_color,
|
||||
on_press=self.on_submit_button_pressed
|
||||
)
|
||||
self.animated_text = AnimatedText(
|
||||
margin_top=2,
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
margin_bottom=2
|
||||
on_press=self.on_submit_button_pressed,
|
||||
is_loading=self.submit_button_loading
|
||||
)
|
||||
return Column(
|
||||
MainViewContentBox(
|
||||
@@ -166,12 +190,12 @@ class RegisterPage(Component):
|
||||
margin_bottom=2,
|
||||
align_x=0.5
|
||||
),
|
||||
self.user_name_input,
|
||||
self.email_input,
|
||||
self.pw_1,
|
||||
self.pw_2,
|
||||
self.submit_button,
|
||||
self.animated_text
|
||||
user_name_input,
|
||||
email_input,
|
||||
pw_1_input,
|
||||
pw_2_input,
|
||||
submit_button,
|
||||
Text(self.display_text, margin_top=2, margin_left=1, margin_right=1, margin_bottom=2, style=self.display_text_style)
|
||||
)
|
||||
),
|
||||
align_y=0,
|
||||
|
||||
@@ -12,8 +12,8 @@ from src.ezgg_lan_manager.components.SeatingPlanInfoBox import SeatingPlanInfoBo
|
||||
from src.ezgg_lan_manager.components.SeatingPurchaseBox import SeatingPurchaseBox
|
||||
from src.ezgg_lan_manager.services.SeatingService import NoTicketError, SeatNotFoundError, WrongCategoryError, SeatAlreadyTakenError
|
||||
from src.ezgg_lan_manager.types.Seat import Seat
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
@@ -37,7 +37,10 @@ class SeatingPlanPage(Component):
|
||||
async def on_populate(self) -> None:
|
||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Sitzplan")
|
||||
self.seating_info = await self.session[SeatingService].get_seating()
|
||||
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||
try:
|
||||
self.user = await self.session[UserService].get_user(self.session[UserSession].user_id)
|
||||
except KeyError:
|
||||
self.user = None
|
||||
if not self.user:
|
||||
self.is_booking_blocked = True
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
from asyncio import sleep
|
||||
|
||||
from rio import event, ProgressCircle, PointerEventListener, PointerEvent, Popup, Color
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService
|
||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||
from src.ezgg_lan_manager.components.TeamRevealer import TeamRevealer
|
||||
from src.ezgg_lan_manager.components.TeamsDialogHandler import *
|
||||
from src.ezgg_lan_manager.services.RefreshService import RefreshService
|
||||
from src.ezgg_lan_manager.services.TeamService import TeamService
|
||||
from src.ezgg_lan_manager.services.UserService import UserService
|
||||
from src.ezgg_lan_manager.types.Team import Team
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
|
||||
class TeamsPage(Component):
|
||||
all_teams: Optional[list[Team]] = None
|
||||
user: Optional[User] = None
|
||||
|
||||
# Dialog handling
|
||||
popup_open: bool = False
|
||||
join_active: bool = False
|
||||
leave_active: bool = True
|
||||
create_active: bool = False
|
||||
selected_team_for_join_or_leave: Optional[Team] = None
|
||||
|
||||
@event.on_populate
|
||||
async def on_populate(self) -> None:
|
||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Teams")
|
||||
self.session[RefreshService].subscribe(self.on_populate)
|
||||
self.all_teams = await self.session[TeamService].get_all_teams()
|
||||
try:
|
||||
self.user = await self.session[UserService].get_user(self.session[UserSession].user_id)
|
||||
except KeyError:
|
||||
self.user = None
|
||||
|
||||
async def on_join_button_pressed(self, team: Team) -> None:
|
||||
if self.user is None:
|
||||
return
|
||||
self.selected_team_for_join_or_leave = team
|
||||
self.join_active, self.leave_active, self.create_active = True, False, False
|
||||
self.popup_open = True
|
||||
|
||||
async def on_leave_button_pressed(self, team: Team) -> None:
|
||||
if self.user is None:
|
||||
return
|
||||
self.selected_team_for_join_or_leave = team
|
||||
self.join_active, self.leave_active, self.create_active = False, True, False
|
||||
self.popup_open = True
|
||||
|
||||
async def on_create_button_pressed(self, _: PointerEvent) -> None:
|
||||
if self.user is None:
|
||||
return
|
||||
self.join_active, self.leave_active, self.create_active = False, False, True
|
||||
self.popup_open = True
|
||||
|
||||
async def popup_action_cancelled(self) -> None:
|
||||
self.popup_open = False
|
||||
await sleep(0.2) # Waits for the animation to play before resetting its contents
|
||||
self.join_active, self.leave_active, self.create_active = False, False, False
|
||||
self.selected_team_for_join_or_leave = None
|
||||
self.all_teams = await self.session[TeamService].get_all_teams()
|
||||
|
||||
def build(self) -> Component:
|
||||
if self.all_teams is None:
|
||||
return Column(
|
||||
MainViewContentBox(
|
||||
ProgressCircle(
|
||||
color="secondary",
|
||||
align_x=0.5,
|
||||
margin_top=1,
|
||||
margin_bottom=1
|
||||
)
|
||||
),
|
||||
Spacer()
|
||||
)
|
||||
|
||||
team_list = []
|
||||
for team in self.all_teams:
|
||||
team_list.append(
|
||||
TeamRevealer(
|
||||
user=self.user,
|
||||
team=team,
|
||||
mode="leave" if self.user in team.members.keys() else "join",
|
||||
on_button_pressed=self.on_leave_button_pressed if self.user in team.members.keys() else self.on_join_button_pressed
|
||||
)
|
||||
)
|
||||
|
||||
if team_list:
|
||||
team_list[-1].margin_bottom = 1
|
||||
|
||||
own_teams_content = Spacer(grow_x=False, grow_y=False)
|
||||
if self.user is not None:
|
||||
user_team_list = []
|
||||
for team in self.all_teams:
|
||||
if self.user in team.members.keys():
|
||||
user_team_list.append(TeamRevealer(user=self.user, team=team, mode="leave", on_button_pressed=self.on_leave_button_pressed))
|
||||
|
||||
if not user_team_list:
|
||||
user_team_list.append(Text(
|
||||
text="Du bist noch in keinem Team.",
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1
|
||||
),
|
||||
margin_top=1,
|
||||
margin_bottom=1,
|
||||
align_x=0.5
|
||||
))
|
||||
else:
|
||||
user_team_list[-1].margin_bottom = 1
|
||||
own_teams_content = MainViewContentBox(
|
||||
Column(
|
||||
Row(
|
||||
Text(
|
||||
text="Deine Teams",
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
grow_x=True,
|
||||
justify="right",
|
||||
margin_right=3
|
||||
),
|
||||
Column(
|
||||
PointerEventListener(Rectangle(
|
||||
content=Text(text="Team erstellen", style=TextStyle(fill=self.session.theme.background_color, font_size=0.7), margin=0.1, selectable=False),
|
||||
stroke_width=0.1,
|
||||
stroke_color=self.session.theme.hud_color,
|
||||
cursor="pointer",
|
||||
hover_fill=self.session.theme.hud_color,
|
||||
transition_time=0
|
||||
), on_press=self.on_create_button_pressed),
|
||||
Spacer(),
|
||||
margin_right=2
|
||||
),
|
||||
margin_top=1,
|
||||
margin_bottom=1
|
||||
),
|
||||
*user_team_list
|
||||
)
|
||||
)
|
||||
|
||||
return Popup(
|
||||
anchor=Column(
|
||||
own_teams_content,
|
||||
MainViewContentBox(
|
||||
Column(
|
||||
Text(
|
||||
text="Alle Teams",
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
margin_top=1,
|
||||
margin_bottom=1,
|
||||
align_x=0.5
|
||||
),
|
||||
*team_list
|
||||
)
|
||||
),
|
||||
align_y=0,
|
||||
),
|
||||
content=Column(
|
||||
TeamsDialogJoinHandler(is_active=self.join_active, cancel=self.popup_action_cancelled, user=self.user, team=self.selected_team_for_join_or_leave),
|
||||
TeamsDialogLeaveHandler(is_active=self.leave_active, cancel=self.popup_action_cancelled, user=self.user, team=self.selected_team_for_join_or_leave),
|
||||
TeamsDialogCreateHandler(is_active=self.create_active, cancel=self.popup_action_cancelled, user=self.user)
|
||||
),
|
||||
is_open=self.popup_open,
|
||||
modal=False,
|
||||
corner_radius=(0.5, 0.5, 0.5, 0.5),
|
||||
color=Color.TRANSPARENT,
|
||||
user_closable=False,
|
||||
position="top"
|
||||
)
|
||||
@@ -1,23 +1,37 @@
|
||||
import logging
|
||||
from asyncio import sleep
|
||||
from functools import partial
|
||||
from typing import Optional, Union, Literal
|
||||
|
||||
from from_root import from_root
|
||||
from rio import Column, Component, event, TextStyle, Text, Row, Image, Spacer, ProgressCircle, Button, Checkbox, ThemeContextSwitcher, Link
|
||||
from rio import Column, Component, event, TextStyle, Text, Row, Image, Spacer, ProgressCircle, Button, Checkbox, ThemeContextSwitcher, Link, Revealer, PointerEventListener, \
|
||||
PointerEvent, Rectangle, Color, Popup, Dropdown
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService, TournamentService, UserService
|
||||
from src.ezgg_lan_manager import ConfigurationService, TournamentService, UserService, TicketingService, TeamService
|
||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||
from src.ezgg_lan_manager.components.TournamentDetailsInfoRow import TournamentDetailsInfoRow
|
||||
from src.ezgg_lan_manager.types.DateUtil import weekday_to_display_text
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.Team import Team, TeamStatus
|
||||
from src.ezgg_lan_manager.types.Tournament import Tournament
|
||||
from src.ezgg_lan_manager.types.TournamentBase import TournamentStatus, tournament_status_to_display_text, tournament_format_to_display_texts
|
||||
from src.ezgg_lan_manager.types.TournamentBase import TournamentStatus, tournament_status_to_display_text, tournament_format_to_display_texts, ParticipantType
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
|
||||
class TournamentDetailsPage(Component):
|
||||
tournament: Optional[Union[Tournament, str]] = None
|
||||
rules_accepted: bool = False
|
||||
user: Optional[User] = None
|
||||
user_teams: list[Team] = []
|
||||
loading: bool = False
|
||||
participant_revealer_open: bool = False
|
||||
current_tournament_user_or_team_list: Union[list[User], list[Team]] = []
|
||||
team_dialog_open: bool = False
|
||||
team_register_options: dict[str, Optional[Team]] = {"": None}
|
||||
team_selected_for_register: Optional[Team] = None
|
||||
|
||||
# State for message above register button
|
||||
message: str = ""
|
||||
is_success: bool = False
|
||||
@@ -32,33 +46,131 @@ class TournamentDetailsPage(Component):
|
||||
self.tournament = await self.session[TournamentService].get_tournament_by_id(tournament_id)
|
||||
if self.tournament is not None:
|
||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - {self.tournament.name}")
|
||||
if self.tournament.participant_type == ParticipantType.PLAYER:
|
||||
self.current_tournament_user_or_team_list = await self.session[TournamentService].get_users_from_participant_list(self.tournament.participants)
|
||||
elif self.tournament.participant_type == ParticipantType.TEAM:
|
||||
self.current_tournament_user_or_team_list = await self.session[TournamentService].get_teams_from_participant_list(self.tournament.participants)
|
||||
else:
|
||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Turniere")
|
||||
|
||||
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||
try:
|
||||
user_id = self.session[UserSession].user_id
|
||||
self.user = await self.session[UserService].get_user(user_id)
|
||||
self.user_teams = await self.session[TeamService].get_teams_for_user_by_id(user_id)
|
||||
except KeyError:
|
||||
self.user = None
|
||||
self.user_teams = []
|
||||
|
||||
self.loading_done()
|
||||
|
||||
@staticmethod
|
||||
async def artificial_delay() -> None:
|
||||
await sleep(0.8) # https://medium.com/design-bootcamp/ux-psychology-of-artificial-waiting-enhancing-user-experiences-through-deliberate-delays-d7822faf3930
|
||||
|
||||
async def update(self) -> None:
|
||||
self.tournament = await self.session[TournamentService].get_tournament_by_id(self.tournament.id)
|
||||
if self.tournament is None:
|
||||
return
|
||||
if self.tournament.participant_type == ParticipantType.PLAYER:
|
||||
self.current_tournament_user_or_team_list = await self.session[TournamentService].get_users_from_participant_list(self.tournament.participants)
|
||||
elif self.tournament.participant_type == ParticipantType.TEAM:
|
||||
self.current_tournament_user_or_team_list = await self.session[TournamentService].get_teams_from_participant_list(self.tournament.participants)
|
||||
|
||||
def open_close_participant_revealer(self, _: PointerEvent) -> None:
|
||||
self.participant_revealer_open = not self.participant_revealer_open
|
||||
|
||||
async def register_pressed(self) -> None:
|
||||
self.loading = True
|
||||
if not self.user:
|
||||
return
|
||||
self.is_success = True
|
||||
self.message = f"Erfolgreich angemeldet!" # ToDo: Hook into Tournament Service
|
||||
|
||||
user_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id)
|
||||
if user_ticket is None:
|
||||
self.is_success = False
|
||||
self.message = "Turnieranmeldung nur mit Ticket"
|
||||
else:
|
||||
# Register single player
|
||||
if self.tournament.participant_type == ParticipantType.PLAYER:
|
||||
try:
|
||||
await self.session[TournamentService].register_user_for_tournament(self.user.user_id, self.tournament.id)
|
||||
await self.artificial_delay()
|
||||
self.is_success = True
|
||||
self.message = f"Erfolgreich angemeldet!"
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
self.is_success = False
|
||||
self.message = f"Fehler: {e}"
|
||||
# Register team
|
||||
elif self.tournament.participant_type == ParticipantType.TEAM:
|
||||
try:
|
||||
team_register_options = {"": None}
|
||||
for team in self.user_teams:
|
||||
if team.members[self.user] == TeamStatus.OFFICER or team.members[self.user] == TeamStatus.LEADER:
|
||||
team_register_options[team.name] = team
|
||||
if team_register_options:
|
||||
self.team_register_options = team_register_options
|
||||
else:
|
||||
self.team_register_options = {"": None}
|
||||
except StopIteration as e:
|
||||
logger.error(f"Error trying to teams to register: {e}")
|
||||
self.team_dialog_open = True
|
||||
return # Model should handle loading state now
|
||||
else:
|
||||
pass
|
||||
await self.update()
|
||||
self.loading = False
|
||||
|
||||
async def unregister_pressed(self) -> None:
|
||||
async def on_team_register_confirmed(self) -> None:
|
||||
if self.team_selected_for_register is None:
|
||||
await self.on_team_register_canceled()
|
||||
return
|
||||
try:
|
||||
await self.session[TournamentService].register_team_for_tournament(self.team_selected_for_register.id, self.tournament.id)
|
||||
await self.artificial_delay()
|
||||
self.is_success = True
|
||||
self.message = f"Erfolgreich angemeldet!"
|
||||
self.team_dialog_open = False
|
||||
self.team_selected_for_register = None
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
self.message = f"Fehler: {e}"
|
||||
self.is_success = False
|
||||
await self.update()
|
||||
self.loading = False
|
||||
|
||||
async def on_team_register_canceled(self) -> None:
|
||||
self.team_dialog_open = False
|
||||
self.team_selected_for_register = None
|
||||
self.loading = False
|
||||
|
||||
async def unregister_pressed(self, team: Optional[Team] = None) -> None:
|
||||
self.loading = True
|
||||
if not self.user:
|
||||
return
|
||||
self.is_success = True
|
||||
self.message = f"Erfolgreich abgemeldet!" # ToDo: Hook into Tournament Service
|
||||
|
||||
try:
|
||||
if self.tournament.participant_type == ParticipantType.PLAYER:
|
||||
await self.session[TournamentService].unregister_user_from_tournament(self.user.user_id, self.tournament.id)
|
||||
elif self.tournament.participant_type == ParticipantType.TEAM:
|
||||
if team.members[self.user] == TeamStatus.OFFICER or team.members[self.user] == TeamStatus.LEADER:
|
||||
await self.session[TournamentService].unregister_team_from_tournament(team.id, self.tournament.id)
|
||||
else:
|
||||
raise PermissionError("Nur Leiter und Offiziere können das Team abmelden")
|
||||
await self.artificial_delay()
|
||||
self.is_success = True
|
||||
self.message = f"Erfolgreich abgemeldet!"
|
||||
except Exception as e:
|
||||
self.is_success = False
|
||||
self.message = f"Fehler: {e}"
|
||||
await self.update()
|
||||
self.loading = False
|
||||
|
||||
async def tree_button_clicked(self) -> None:
|
||||
pass # ToDo: Implement tournament tree view
|
||||
|
||||
def loading_done(self) -> None:
|
||||
if self.tournament is None:
|
||||
self.tournament = "Turnier konnte nicht gefunden werden"
|
||||
self.session[SessionStorage].subscribe_to_logged_in_or_out_event(str(self.__class__), self.on_populate)
|
||||
|
||||
def build(self) -> Component:
|
||||
if self.tournament is None:
|
||||
@@ -86,30 +198,65 @@ class TournamentDetailsPage(Component):
|
||||
)
|
||||
else:
|
||||
tournament_status_color = self.session.theme.background_color
|
||||
tree_button = Spacer(grow_x=False, grow_y=False)
|
||||
if self.tournament.status == TournamentStatus.OPEN:
|
||||
tournament_status_color = self.session.theme.success_color
|
||||
elif self.tournament.status == TournamentStatus.CLOSED:
|
||||
tournament_status_color = self.session.theme.danger_color
|
||||
elif self.tournament.status == TournamentStatus.ONGOING:
|
||||
elif self.tournament.status == TournamentStatus.ONGOING or self.tournament.status == TournamentStatus.COMPLETED:
|
||||
tournament_status_color = self.session.theme.warning_color
|
||||
tree_button = Button(
|
||||
content="Turnierbaum anzeigen",
|
||||
shape="rectangle",
|
||||
style="minor",
|
||||
color="hud",
|
||||
margin_left=4,
|
||||
margin_right=4,
|
||||
margin_top=1,
|
||||
on_press=self.tree_button_clicked
|
||||
)
|
||||
|
||||
|
||||
# ToDo: Integrate Teams logic
|
||||
ids_of_participants = [p.id for p in self.tournament.participants]
|
||||
color_key: Literal["hud", "danger"] = "hud"
|
||||
on_press_function = self.register_pressed
|
||||
if self.user and self.user.user_id in ids_of_participants: # User already registered for tournament
|
||||
button_text = "Abmelden"
|
||||
button_sensitive_hook = True # User has already accepted the rules previously
|
||||
color_key = "danger"
|
||||
on_press_function = self.unregister_pressed
|
||||
elif self.user and self.user.user_id not in ids_of_participants:
|
||||
button_text = "Anmelden"
|
||||
button_sensitive_hook = self.rules_accepted
|
||||
if self.tournament.participant_type == ParticipantType.PLAYER:
|
||||
self.current_tournament_user_or_team_list: list[User] # IDE TypeHint
|
||||
participant_names = "\n".join([u.user_name for u in self.current_tournament_user_or_team_list])
|
||||
if self.user and self.user.user_id in ids_of_participants: # User already registered for tournament
|
||||
button_text = "Abmelden"
|
||||
button_sensitive_hook = True # User has already accepted the rules previously
|
||||
color_key = "danger"
|
||||
on_press_function = self.unregister_pressed
|
||||
elif self.user and self.user.user_id not in ids_of_participants:
|
||||
button_text = "Anmelden"
|
||||
button_sensitive_hook = self.rules_accepted
|
||||
else:
|
||||
# This should NEVER happen
|
||||
button_text = "Anmelden"
|
||||
button_sensitive_hook = False
|
||||
elif self.tournament.participant_type == ParticipantType.TEAM:
|
||||
self.current_tournament_user_or_team_list: list[Team] # IDE TypeHint
|
||||
participant_names = "\n".join([t.name for t in self.current_tournament_user_or_team_list])
|
||||
user_team_registered = []
|
||||
for team in self.user_teams:
|
||||
if team.id in ids_of_participants:
|
||||
user_team_registered.append(team)
|
||||
if self.user and len(user_team_registered) > 0: # Any of the users teams already registered for tournament
|
||||
button_text = f"{user_team_registered[0].abbreviation} abmelden"
|
||||
button_sensitive_hook = True # User has already accepted the rules previously
|
||||
color_key = "danger"
|
||||
on_press_function = partial(self.unregister_pressed, user_team_registered[0])
|
||||
elif self.user and len(user_team_registered) == 0:
|
||||
button_text = "Anmelden"
|
||||
button_sensitive_hook = self.rules_accepted
|
||||
else:
|
||||
# This should NEVER happen
|
||||
button_text = "Anmelden"
|
||||
button_sensitive_hook = False
|
||||
else:
|
||||
# This should NEVER happen
|
||||
button_text = "Anmelden"
|
||||
button_sensitive_hook = False
|
||||
logger.fatal("Did someone add new values to ParticipantType ? ;)")
|
||||
return Column()
|
||||
|
||||
|
||||
if self.tournament.status != TournamentStatus.OPEN or self.tournament.is_full:
|
||||
button_sensitive_hook = False # Override button controls if tournament is not open anymore or full
|
||||
@@ -118,7 +265,7 @@ class TournamentDetailsPage(Component):
|
||||
accept_rules_row = Row(
|
||||
ThemeContextSwitcher(content=Checkbox(is_on=self.bind().rules_accepted, margin_left=4), color=self.session.theme.hud_color),
|
||||
Text("Ich akzeptiere die ", margin_left=1, style=TextStyle(fill=self.session.theme.background_color, font_size=0.8), overflow="nowrap", justify="right"),
|
||||
Link(Text("Turnierregeln", margin_right=4, style=TextStyle(fill=self.session.theme.background_color, font_size=0.8, italic=True), overflow="nowrap", justify="left"), "https://google.de") # ToDo: Add rules
|
||||
Link(Text("Turnierregeln", margin_right=4, style=TextStyle(fill=self.session.theme.background_color, font_size=0.8, italic=True), overflow="nowrap", justify="left"), "./tournament-rules", open_in_new_tab=True)
|
||||
)
|
||||
button = Button(
|
||||
content=button_text,
|
||||
@@ -135,11 +282,9 @@ class TournamentDetailsPage(Component):
|
||||
# No UI here if user not logged in
|
||||
accept_rules_row, button = Spacer(), Spacer()
|
||||
|
||||
|
||||
|
||||
content = Column(
|
||||
Row(
|
||||
Image(image=from_root(f"src/ezgg_lan_manager/assets/img/games/{self.tournament.game_title.image_name}")),
|
||||
Image(image=from_root(f"src/ezgg_lan_manager/assets/img/games/{self.tournament.game_title.image_name}"), margin_right=1),
|
||||
Text(
|
||||
text=self.tournament.name,
|
||||
style=TextStyle(
|
||||
@@ -154,15 +299,34 @@ class TournamentDetailsPage(Component):
|
||||
margin_left=6
|
||||
),
|
||||
Spacer(min_height=1),
|
||||
TournamentDetailsInfoRow("Status", tournament_status_to_display_text(self.tournament.status), tournament_status_color),
|
||||
TournamentDetailsInfoRow("Status", tournament_status_to_display_text(self.tournament.status), value_color=tournament_status_color),
|
||||
TournamentDetailsInfoRow("Startzeit", f"{weekday_to_display_text(self.tournament.start_time.weekday())}, {self.tournament.start_time.strftime('%H:%M')} Uhr"),
|
||||
TournamentDetailsInfoRow("Format", tournament_format_to_display_texts(self.tournament.format)[0]),
|
||||
TournamentDetailsInfoRow("Best of", tournament_format_to_display_texts(self.tournament.format)[1]),
|
||||
TournamentDetailsInfoRow(
|
||||
"Teilnehmer",
|
||||
f"{len(self.tournament.participants)} / {self.tournament.max_participants}",
|
||||
self.session.theme.danger_color if self.tournament.is_full else self.session.theme.background_color
|
||||
PointerEventListener(
|
||||
content=Rectangle(
|
||||
content=TournamentDetailsInfoRow(
|
||||
"Teilnehmer ▴" if self.participant_revealer_open else "Teilnehmer ▾",
|
||||
f"{len(self.current_tournament_user_or_team_list)} / {self.tournament.max_participants}",
|
||||
value_color=self.session.theme.danger_color if self.tournament.is_full else self.session.theme.background_color,
|
||||
key_color=self.session.theme.secondary_color
|
||||
),
|
||||
fill=Color.TRANSPARENT,
|
||||
cursor="pointer"
|
||||
),
|
||||
on_press=self.open_close_participant_revealer
|
||||
),
|
||||
Revealer(
|
||||
header=None,
|
||||
content=Text(
|
||||
participant_names,
|
||||
style=TextStyle(fill=self.session.theme.background_color)
|
||||
),
|
||||
is_open=self.participant_revealer_open,
|
||||
margin_left=4,
|
||||
margin_right=4
|
||||
),
|
||||
tree_button,
|
||||
Row(
|
||||
Text(
|
||||
text="Info",
|
||||
@@ -185,6 +349,39 @@ class TournamentDetailsPage(Component):
|
||||
button
|
||||
)
|
||||
|
||||
if self.tournament and self.tournament.participant_type == ParticipantType.TEAM:
|
||||
content = Popup(
|
||||
anchor=content,
|
||||
content=Rectangle(
|
||||
content=Column(
|
||||
Text("Welches Team anmelden?", style=TextStyle(fill=self.session.theme.background_color, font_size=1.2), justify="center", margin_bottom=1),
|
||||
ThemeContextSwitcher(
|
||||
content=Dropdown(
|
||||
options=self.team_register_options,
|
||||
min_width=20,
|
||||
selected_value=self.bind().team_selected_for_register
|
||||
),
|
||||
color=self.session.theme.hud_color,
|
||||
margin_bottom=1
|
||||
),
|
||||
Row(
|
||||
Button(content="Abbrechen", shape="rectangle", grow_x=False, on_press=self.on_team_register_canceled),
|
||||
Button(content="Anmelden", shape="rectangle", grow_x=False, on_press=self.on_team_register_confirmed),
|
||||
spacing=1
|
||||
),
|
||||
margin=0.5
|
||||
),
|
||||
min_width=30,
|
||||
min_height=4,
|
||||
fill=self.session.theme.primary_color,
|
||||
margin_top=3.5,
|
||||
stroke_width=0.3,
|
||||
stroke_color=self.session.theme.neutral_color,
|
||||
),
|
||||
is_open=self.team_dialog_open,
|
||||
color="none"
|
||||
)
|
||||
|
||||
return Column(
|
||||
MainViewContentBox(
|
||||
Column(
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
from rio import Column, Component, event, TextStyle, Text, Spacer
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService
|
||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||
|
||||
RULES: list[str] = [
|
||||
"Den Anweisungen der Turnierleitung ist stets Folge zu leisten.",
|
||||
"Teilnehmer müssen aktiv dafür sorgen, dass Spiele ohne Verzögerungen stattfinden.",
|
||||
"Unvollständige Teams werden ggf. zum Turnierstart entfernt.",
|
||||
"Verzögerungen und Ausfälle sind er Turnierleitung sofort zu melden.",
|
||||
"Jeder Spieler erstellt Screenshots am Rundenende zur Ergebnisdokumentation.",
|
||||
"Der Verlierer trägt das Ergebnis ein, der Gewinner überprüft es.",
|
||||
"Bei fehlendem oder falschem Ergebnis, ist sofort die Turnierorganisation zu informieren.",
|
||||
"Von 02:00–11:00 Uhr besteht keine Spielpflicht",
|
||||
"Täuschung, Falschangaben sowie Bugusing und Cheaten führen zur sofortigen Disqualifikation."
|
||||
]
|
||||
|
||||
class TournamentRulesPage(Component):
|
||||
@event.on_populate
|
||||
async def on_populate(self) -> None:
|
||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Turnierregeln")
|
||||
|
||||
def build(self) -> Component:
|
||||
return Column(
|
||||
MainViewContentBox(
|
||||
Column(
|
||||
Text(
|
||||
text="Turnierregeln",
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
margin_top=2,
|
||||
margin_bottom=2,
|
||||
align_x=0.5
|
||||
),
|
||||
*[Text(
|
||||
f"{idx + 1}. {rule}",
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=0.9
|
||||
),
|
||||
margin_bottom=0.8,
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
overflow="wrap"
|
||||
) for idx, rule in enumerate(RULES)],
|
||||
Spacer(min_height=1)
|
||||
)
|
||||
),
|
||||
Spacer(grow_y=True)
|
||||
)
|
||||
@@ -21,3 +21,7 @@ from .ManageCateringPage import ManageCateringPage
|
||||
from .ManageTournamentsPage import ManageTournamentsPage
|
||||
from .OverviewPage import OverviewPage
|
||||
from .TournamentDetailsPage import TournamentDetailsPage
|
||||
from .TournamentRulesPage import TournamentRulesPage
|
||||
from .ConwayPage import ConwayPage
|
||||
from .TeamsPage import TeamsPage
|
||||
from .AdminNavigationPage import AdminNavigationPage
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import io
|
||||
import logging
|
||||
import qrcode
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, ROUND_DOWN
|
||||
@@ -74,3 +76,29 @@ class AccountingService:
|
||||
return "0.00 €"
|
||||
rounded_decimal = str(euros.quantize(Decimal(".01"), rounding=ROUND_DOWN))
|
||||
return f"{rounded_decimal} €"
|
||||
|
||||
@staticmethod
|
||||
def make_payment_qr_image(beneficiary_name, beneficiary_bic, beneficiary_iban, text, amount_euros=None) -> bytes:
|
||||
text = text.replace("\n",";")
|
||||
amount_formatted = "EUR{:.2f}".format(amount_euros) if amount_euros else ""
|
||||
epc_text = f"""BCD
|
||||
002
|
||||
1
|
||||
SCT
|
||||
{beneficiary_bic}
|
||||
{beneficiary_name}
|
||||
{beneficiary_iban}
|
||||
{amount_formatted}
|
||||
|
||||
|
||||
{text}
|
||||
"""
|
||||
qr = qrcode.QRCode(
|
||||
version=6,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_M,
|
||||
)
|
||||
qr.add_data(epc_text)
|
||||
img = qr.make_image()
|
||||
img_bytes = io.BytesIO()
|
||||
img.save(img_bytes)
|
||||
return img_bytes.getvalue()
|
||||
|
||||
@@ -11,8 +11,12 @@ from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItem, Cateri
|
||||
from src.ezgg_lan_manager.types.CateringOrder import CateringMenuItemsWithAmount, CateringOrderStatus
|
||||
from src.ezgg_lan_manager.types.ConfigurationTypes import DatabaseConfiguration
|
||||
from src.ezgg_lan_manager.types.News import News
|
||||
from src.ezgg_lan_manager.types.Participant import Participant
|
||||
from src.ezgg_lan_manager.types.Seat import Seat
|
||||
from src.ezgg_lan_manager.types.Team import TeamStatus, Team
|
||||
from src.ezgg_lan_manager.types.Ticket import Ticket
|
||||
from src.ezgg_lan_manager.types.Tournament import Tournament
|
||||
from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, ParticipantType
|
||||
from src.ezgg_lan_manager.types.Transaction import Transaction
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
@@ -58,7 +62,8 @@ class DatabaseService:
|
||||
password=self._database_config.db_password,
|
||||
db=self._database_config.db_name,
|
||||
minsize=1,
|
||||
maxsize=40
|
||||
maxsize=40,
|
||||
autocommit=True
|
||||
)
|
||||
except aiomysql.OperationalError:
|
||||
return False
|
||||
@@ -71,16 +76,65 @@ class DatabaseService:
|
||||
user_name=data[1],
|
||||
user_mail=data[2],
|
||||
user_password=data[3],
|
||||
user_first_name=data[4],
|
||||
user_last_name=data[5],
|
||||
user_birth_day=data[6],
|
||||
is_active=bool(data[7]),
|
||||
is_team_member=bool(data[8]),
|
||||
is_admin=bool(data[9]),
|
||||
created_at=data[10],
|
||||
last_updated_at=data[11]
|
||||
user_fallback_password=data[4],
|
||||
user_first_name=data[5],
|
||||
user_last_name=data[6],
|
||||
user_birth_day=data[7],
|
||||
is_active=bool(data[8]),
|
||||
is_team_member=bool(data[9]),
|
||||
is_admin=bool(data[10]),
|
||||
created_at=data[11],
|
||||
last_updated_at=data[12]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _parse_tournament_format(format_as_string: str) -> TournamentFormat:
|
||||
if format_as_string == "SE_BO_1":
|
||||
return TournamentFormat.SINGLE_ELIMINATION_BO_1
|
||||
elif format_as_string == "SE_BO_3":
|
||||
return TournamentFormat.SINGLE_ELIMINATION_BO_3
|
||||
elif format_as_string == "SE_BO_5":
|
||||
return TournamentFormat.SINGLE_ELIMINATION_BO_5
|
||||
elif format_as_string == "DE_BO_1":
|
||||
return TournamentFormat.DOUBLE_ELIMINATION_BO_1
|
||||
elif format_as_string == "DE_BO_3":
|
||||
return TournamentFormat.DOUBLE_ELIMINATION_BO_3
|
||||
elif format_as_string == "DE_BO_5":
|
||||
return TournamentFormat.DOUBLE_ELIMINATION_BO_5
|
||||
elif format_as_string == "FFA":
|
||||
return TournamentFormat.FFA
|
||||
else:
|
||||
# If this happens, database is FUBAR
|
||||
raise RuntimeError(f"Unknown TournamentFormat: {format_as_string}")
|
||||
|
||||
@staticmethod
|
||||
def _parse_tournament_status(status_as_string: str) -> TournamentStatus:
|
||||
if status_as_string == "CLOSED":
|
||||
return TournamentStatus.CLOSED
|
||||
elif status_as_string == "OPEN":
|
||||
return TournamentStatus.OPEN
|
||||
elif status_as_string == "COMPLETED":
|
||||
return TournamentStatus.COMPLETED
|
||||
elif status_as_string == "CANCELED":
|
||||
return TournamentStatus.CANCELED
|
||||
elif status_as_string == "INVITE_ONLY":
|
||||
return TournamentStatus.INVITE_ONLY
|
||||
elif status_as_string == "ONGOING":
|
||||
return TournamentStatus.ONGOING
|
||||
else:
|
||||
# If this happens, database is FUBAR
|
||||
raise RuntimeError(f"Unknown TournamentStatus: {status_as_string}")
|
||||
|
||||
@staticmethod
|
||||
def _parse_participant_type(participant_type_as_string: str) -> ParticipantType:
|
||||
if participant_type_as_string == "PLAYER":
|
||||
return ParticipantType.PLAYER
|
||||
elif participant_type_as_string == "TEAM":
|
||||
return ParticipantType.TEAM
|
||||
else:
|
||||
# If this happens, database is FUBAR
|
||||
raise RuntimeError(f"Unknown ParticipantType: {participant_type_as_string}")
|
||||
|
||||
async def get_user_by_name(self, user_name: str) -> Optional[User]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
@@ -133,10 +187,10 @@ class DatabaseService:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"UPDATE users SET user_name=%s, user_mail=%s, user_password=%s, user_first_name=%s, "
|
||||
"user_last_name=%s, user_birth_date=%s, is_active=%s, is_team_member=%s, is_admin=%s "
|
||||
"WHERE (user_id=%s)",
|
||||
(user.user_name, user.user_mail.lower(), user.user_password,
|
||||
"UPDATE users SET user_name=%s, user_mail=%s, user_password=%s, user_fallback_password=%s,"
|
||||
"user_first_name=%s, user_last_name=%s, user_birth_date=%s, is_active=%s, is_team_member=%s,"
|
||||
" is_admin=%s WHERE (user_id=%s)",
|
||||
(user.user_name, user.user_mail.lower(), user.user_password, user.user_fallback_password,
|
||||
user.user_first_name, user.user_last_name, user.user_birth_day,
|
||||
user.is_active, user.is_team_member, user.is_admin,
|
||||
user.user_id)
|
||||
@@ -395,7 +449,7 @@ class DatabaseService:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.change_ticket_owner(ticket_id)
|
||||
return await self.delete_ticket(ticket_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error deleting ticket: {e}")
|
||||
return False
|
||||
@@ -787,3 +841,347 @@ class DatabaseService:
|
||||
return await self.remove_profile_picture(user_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error deleting user profile picture: {e}")
|
||||
|
||||
async def get_all_tournaments(self) -> list[Tournament]:
|
||||
logger.info(f"Polling Tournaments...")
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.DictCursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"""
|
||||
SELECT
|
||||
/* =======================
|
||||
Tournament
|
||||
======================= */
|
||||
t.id AS tournament_id,
|
||||
t.name AS tournament_name,
|
||||
t.description AS tournament_description,
|
||||
t.format AS tournament_format,
|
||||
t.start_time,
|
||||
t.status AS tournament_status,
|
||||
t.max_participants,
|
||||
t.created_at,
|
||||
t.participant_type AS tournament_participant_type,
|
||||
|
||||
/* =======================
|
||||
Game Title
|
||||
======================= */
|
||||
gt.id AS game_title_id,
|
||||
gt.name AS game_title_name,
|
||||
gt.description AS game_title_description,
|
||||
gt.web_link AS game_title_web_link,
|
||||
gt.image_name AS game_title_image_name,
|
||||
|
||||
/* =======================
|
||||
Tournament Participant
|
||||
======================= */
|
||||
tp.id AS participant_id,
|
||||
tp.user_id,
|
||||
tp.team_id,
|
||||
tp.participant_type,
|
||||
tp.seed,
|
||||
tp.joined_at
|
||||
|
||||
FROM tournaments t
|
||||
JOIN game_titles gt
|
||||
ON gt.id = t.game_title_id
|
||||
|
||||
LEFT JOIN tournament_participants tp
|
||||
ON tp.tournament_id = t.id
|
||||
|
||||
ORDER BY
|
||||
t.id,
|
||||
tp.seed IS NULL,
|
||||
tp.seed;
|
||||
|
||||
"""
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_all_tournaments()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting tournaments: {e}")
|
||||
|
||||
tournaments = []
|
||||
current_tournament: Optional[Tournament] = None
|
||||
for row in await cursor.fetchall():
|
||||
if current_tournament is None or current_tournament.id != row["tournament_id"]:
|
||||
if current_tournament is not None:
|
||||
tournaments.append(current_tournament)
|
||||
participant_type = self._parse_participant_type(row["tournament_participant_type"])
|
||||
id_accessor = "user_id" if participant_type == ParticipantType.PLAYER else "team_id"
|
||||
current_tournament = Tournament(
|
||||
id_=row["tournament_id"],
|
||||
name=row["tournament_name"],
|
||||
description=row["tournament_description"],
|
||||
game_title=GameTitle(
|
||||
name=row["game_title_name"],
|
||||
description=row["game_title_description"],
|
||||
web_link=row["game_title_web_link"],
|
||||
image_name=row["game_title_image_name"]
|
||||
),
|
||||
format_=self._parse_tournament_format(row["tournament_format"]),
|
||||
start_time=row["start_time"],
|
||||
status=self._parse_tournament_status(row["tournament_status"]),
|
||||
participants=[Participant(id_=row[id_accessor], participant_type=self._parse_participant_type(row["participant_type"]))] if row[id_accessor] is not None else [],
|
||||
matches=None, # ToDo: Implement
|
||||
rounds=[], # ToDo: Implement
|
||||
max_participants=row["max_participants"],
|
||||
participant_type=participant_type
|
||||
)
|
||||
else:
|
||||
id_accessor = "user_id" if current_tournament.participant_type == ParticipantType.PLAYER else "team_id"
|
||||
current_tournament.add_participant(
|
||||
Participant(id_=row[id_accessor], participant_type=self._parse_participant_type(row["participant_type"]))
|
||||
)
|
||||
else:
|
||||
tournaments.append(current_tournament)
|
||||
|
||||
return tournaments
|
||||
|
||||
async def add_participant_to_tournament(self, participant: Participant, tournament: Tournament) -> None:
|
||||
if participant.participant_type != tournament.participant_type:
|
||||
raise ValueError(f"Can not add {participant.participant_type.name} to {tournament.participant_type.name} tournament")
|
||||
accessor = "user_id" if participant.participant_type == ParticipantType.PLAYER else "team_id"
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
f"INSERT INTO tournament_participants (tournament_id, {accessor}, participant_type) VALUES (%s, %s, %s);",
|
||||
(tournament.id, participant.id, participant.participant_type.name)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.add_participant_to_tournament(participant, tournament)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error adding participant to tournament: {e}")
|
||||
|
||||
async def remove_participant_from_tournament(self, participant: Participant, tournament: Tournament) -> None:
|
||||
accessor = "user_id" if participant.participant_type == ParticipantType.PLAYER else "team_id"
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
f"DELETE FROM tournament_participants WHERE (tournament_id = %s AND {accessor} = %s);",
|
||||
(tournament.id, participant.id)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.remove_participant_from_tournament(participant, tournament)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error removing participant from tournament: {e}")
|
||||
|
||||
async def get_teams(self) -> list[Team]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
query = """
|
||||
SELECT
|
||||
t.id AS team_id,
|
||||
t.name AS team_name,
|
||||
t.abbreviation AS team_abbr,
|
||||
t.join_password,
|
||||
t.created_at AS team_created_at,
|
||||
|
||||
tm.status AS team_status,
|
||||
tm.joined_at AS member_joined_at,
|
||||
|
||||
u.*
|
||||
|
||||
FROM teams t
|
||||
|
||||
LEFT JOIN team_members tm
|
||||
ON t.id = tm.team_id
|
||||
|
||||
LEFT JOIN users u
|
||||
ON tm.user_id = u.user_id
|
||||
|
||||
ORDER BY
|
||||
t.id,
|
||||
CASE tm.status
|
||||
WHEN 'LEADER' THEN 1
|
||||
WHEN 'OFFICER' THEN 2
|
||||
WHEN 'MEMBER' THEN 3
|
||||
ELSE 4
|
||||
END,
|
||||
u.user_name;
|
||||
"""
|
||||
try:
|
||||
await cursor.execute(query)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_teams()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting teams: {e}")
|
||||
return []
|
||||
|
||||
current_team: Optional[Team] = None
|
||||
all_teams = []
|
||||
|
||||
for row in await cursor.fetchall():
|
||||
if row[5] is None: # Teams without single member are ignored
|
||||
continue
|
||||
if current_team is None:
|
||||
user = self._map_db_result_to_user(row[7:])
|
||||
current_team = Team(id=row[0], name=row[1], abbreviation=row[2], join_password=row[3], members={user: TeamStatus.from_str(row[5])})
|
||||
elif current_team.id == row[0]: # Still same team
|
||||
current_team.members[self._map_db_result_to_user(row[7:])] = TeamStatus.from_str(row[5])
|
||||
else:
|
||||
all_teams.append(current_team)
|
||||
user = self._map_db_result_to_user(row[7:])
|
||||
current_team = Team(id=row[0], name=row[1], abbreviation=row[2], join_password=row[3], members={user: TeamStatus.from_str(row[5])})
|
||||
|
||||
if current_team is not None:
|
||||
all_teams.append(current_team)
|
||||
|
||||
return all_teams
|
||||
|
||||
async def get_team_by_id(self, team_id: int) -> Optional[Team]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
query = """
|
||||
SELECT
|
||||
t.id AS team_id,
|
||||
t.name AS team_name,
|
||||
t.abbreviation AS team_abbr,
|
||||
t.join_password,
|
||||
t.created_at AS team_created_at,
|
||||
|
||||
tm.status AS team_status,
|
||||
tm.joined_at AS member_joined_at,
|
||||
|
||||
u.*
|
||||
|
||||
FROM teams t
|
||||
|
||||
LEFT JOIN team_members tm
|
||||
ON t.id = tm.team_id
|
||||
|
||||
LEFT JOIN users u
|
||||
ON tm.user_id = u.user_id
|
||||
|
||||
WHERE t.id = %s
|
||||
|
||||
ORDER BY
|
||||
t.id,
|
||||
CASE tm.status
|
||||
WHEN 'LEADER' THEN 1
|
||||
WHEN 'OFFICER' THEN 2
|
||||
WHEN 'MEMBER' THEN 3
|
||||
ELSE 4
|
||||
END,
|
||||
u.user_name;
|
||||
"""
|
||||
try:
|
||||
await cursor.execute(query, (team_id, ))
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_team_by_id(team_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting team: {e}")
|
||||
return None
|
||||
|
||||
team: Optional[Team] = None
|
||||
|
||||
for row in await cursor.fetchall():
|
||||
if team is None:
|
||||
user = self._map_db_result_to_user(row[7:])
|
||||
team = Team(id=row[0], name=row[1], abbreviation=row[2], join_password=row[3], members={user: TeamStatus.from_str(row[5])})
|
||||
elif team.id == row[0]:
|
||||
team.members[self._map_db_result_to_user(row[7:])] = TeamStatus.from_str(row[5])
|
||||
|
||||
return team
|
||||
|
||||
async def create_team(self, team_name: str, team_abbr: str, join_password: str, leader: User) -> Team:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"INSERT INTO teams (name, abbreviation, join_password) "
|
||||
"VALUES (%s, %s, %s)", (team_name, team_abbr, join_password)
|
||||
)
|
||||
await conn.commit()
|
||||
team_id = cursor.lastrowid
|
||||
await cursor.execute(
|
||||
"INSERT INTO team_members (team_id, user_id, status) VALUES (%s, %s, %s)",
|
||||
(team_id, leader.user_id, TeamStatus.LEADER.name)
|
||||
)
|
||||
await conn.commit()
|
||||
return await self.get_team_by_id(team_id)
|
||||
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.create_team(team_name, team_abbr, join_password)
|
||||
except aiomysql.IntegrityError as e:
|
||||
logger.warning(f"Aborted duplication entry: {e}")
|
||||
raise DuplicationError
|
||||
|
||||
async def update_team(self, team: Team) -> Team:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"UPDATE teams SET name = %s, abbreviation = %s, join_password = %s WHERE (id = %s)",
|
||||
(team.name, team.abbreviation, team.join_password, team.id)
|
||||
)
|
||||
await conn.commit()
|
||||
return await self.get_team_by_id(team.id)
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.update_team(team)
|
||||
except aiomysql.IntegrityError as e:
|
||||
logger.warning(f"Aborted duplication entry: {e}")
|
||||
raise DuplicationError
|
||||
|
||||
|
||||
async def add_member_to_team(self, team: Team, user: User, status: TeamStatus = TeamStatus.MEMBER) -> None:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"INSERT INTO team_members (team_id, user_id, status) VALUES (%s, %s, %s)",
|
||||
(team.id, user.user_id, status.name)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.add_member_to_team(team, user, status)
|
||||
except aiomysql.IntegrityError as e:
|
||||
logger.warning(f"Failed to add member {user.user_name} to team {team.name}: {e}")
|
||||
raise DuplicationError
|
||||
|
||||
|
||||
async def remove_user_from_team(self, team: Team, user: User) -> None:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"DELETE FROM team_members WHERE team_id = %s AND user_id = %s",
|
||||
(team.id, user.user_id)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.remove_user_from_team(team, user)
|
||||
|
||||
@@ -3,23 +3,24 @@ from typing import Optional
|
||||
|
||||
from rio import UserSettings
|
||||
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
|
||||
class LocalData(UserSettings):
|
||||
stored_session_token: Optional[str] = None
|
||||
stored_session_token: Optional[str]
|
||||
|
||||
class LocalDataService:
|
||||
def __init__(self) -> None:
|
||||
self._session: dict[str, SessionStorage] = {}
|
||||
self._session: dict[str, UserSession] = {}
|
||||
|
||||
def verify_token(self, token: str) -> Optional[SessionStorage]:
|
||||
def verify_token(self, token: str) -> Optional[UserSession]:
|
||||
return self._session.get(token)
|
||||
|
||||
def set_session(self, session: SessionStorage) -> str:
|
||||
def set_session(self, session: UserSession) -> str:
|
||||
key = secrets.token_hex(32)
|
||||
self._session[key] = session
|
||||
return key
|
||||
|
||||
def del_session(self, token: str) -> None:
|
||||
self._session.pop(token, None)
|
||||
def del_session(self, token: Optional[str]) -> None:
|
||||
if token is not None:
|
||||
self._session.pop(token, None)
|
||||
|
||||
@@ -45,7 +45,7 @@ class MailingService:
|
||||
return f"""
|
||||
Hallo {user.user_name},
|
||||
|
||||
deinem Account wurden {added_balance} € hinzugefügt. Dein neues Guthaben beträgt nun {total_balance} €.
|
||||
deinem Account wurden {added_balance:.2f} € hinzugefügt. Dein neues Guthaben beträgt nun {total_balance:.2f} €.
|
||||
|
||||
Wenn du zu dieser Aufladung Fragen hast, stehen wir dir in unserem Discord Server oder per Mail an {self._configuration_service.get_lan_info().organizer_mail} zur Verfügung.
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class RefreshService:
|
||||
"""
|
||||
rio.Components can subscribe to this service with their on_populate method.
|
||||
Those methods get called whenever a overall refresh is needed. Usually when the user logs in or out.
|
||||
"""
|
||||
def __init__(self) -> None:
|
||||
self.subscribers: set[Callable] = set()
|
||||
|
||||
def subscribe(self, refresh_cb: Callable) -> None:
|
||||
self.subscribers.add(refresh_cb)
|
||||
|
||||
async def trigger_refresh(self) -> None:
|
||||
for refresh_cb in self.subscribers:
|
||||
await refresh_cb()
|
||||
@@ -0,0 +1,134 @@
|
||||
from string import ascii_letters, digits
|
||||
from typing import Optional
|
||||
|
||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService, DuplicationError
|
||||
from src.ezgg_lan_manager.types.Team import TeamStatus, Team
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
|
||||
class NameNotAllowedError(Exception):
|
||||
def __init__(self, disallowed_char: str) -> None:
|
||||
self.disallowed_char = disallowed_char
|
||||
|
||||
|
||||
class AlreadyMemberError(Exception):
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class NotMemberError(Exception):
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class TeamLeadRemovalError(Exception):
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class TeamNameTooLongError(Exception):
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class TeamNameAlreadyTaken(Exception):
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class TeamAbbrInvalidError(Exception):
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class TeamService:
|
||||
ALLOWED_TEAM_NAME_SYMBOLS = ascii_letters + digits + "!#$%&*+,-./:;<=>?[]^_{|}~ "
|
||||
MAX_TEAM_NAME_LENGTH = 24
|
||||
MAX_TEAM_ABBR_LENGTH = 8
|
||||
|
||||
def __init__(self, db_service: DatabaseService) -> None:
|
||||
self._db_service = db_service
|
||||
|
||||
async def get_all_teams(self) -> list[Team]:
|
||||
return await self._db_service.get_teams()
|
||||
|
||||
async def get_team_by_id(self, team_id: int) -> Optional[Team]:
|
||||
return await self._db_service.get_team_by_id(team_id)
|
||||
|
||||
async def get_teams_for_user_by_id(self, user_id: int) -> list[Team]:
|
||||
all_teams = await self.get_all_teams()
|
||||
user_teams = []
|
||||
for team in all_teams:
|
||||
if user_id in [u.user_id for u in team.members.keys()]:
|
||||
user_teams.append(team)
|
||||
return user_teams
|
||||
|
||||
async def create_team(self, team_name: str, team_abbr: str, join_password: str, leader: User) -> Team:
|
||||
disallowed_char = self._check_for_disallowed_char(team_name)
|
||||
if disallowed_char:
|
||||
raise NameNotAllowedError(disallowed_char)
|
||||
disallowed_char = self._check_for_disallowed_char(team_abbr)
|
||||
if disallowed_char:
|
||||
raise NameNotAllowedError(disallowed_char)
|
||||
|
||||
if not team_name or len(team_name) > self.MAX_TEAM_NAME_LENGTH:
|
||||
raise TeamNameTooLongError()
|
||||
|
||||
if not team_abbr or len(team_abbr) > self.MAX_TEAM_ABBR_LENGTH:
|
||||
raise TeamAbbrInvalidError()
|
||||
|
||||
try:
|
||||
created_team = await self._db_service.create_team(team_name, team_abbr, join_password, leader)
|
||||
except DuplicationError:
|
||||
raise TeamNameAlreadyTaken
|
||||
return created_team
|
||||
|
||||
async def update_team(self, team: Team) -> Team:
|
||||
"""
|
||||
Updates the team EXCLUDING adding and removing members. This is to be done via add_member_to_team and remove_member_from_team
|
||||
:param team: New instance of Team that is to be updated
|
||||
:return: The modified Team instance
|
||||
"""
|
||||
disallowed_char = self._check_for_disallowed_char(team.name)
|
||||
if disallowed_char:
|
||||
raise NameNotAllowedError(disallowed_char)
|
||||
disallowed_char = self._check_for_disallowed_char(team.abbreviation)
|
||||
if disallowed_char:
|
||||
raise NameNotAllowedError(disallowed_char)
|
||||
|
||||
if not team.name or len(team.name) > self.MAX_TEAM_NAME_LENGTH:
|
||||
raise TeamNameTooLongError()
|
||||
|
||||
if not team.abbreviation or len(team.abbreviation) > self.MAX_TEAM_ABBR_LENGTH:
|
||||
raise TeamAbbrInvalidError()
|
||||
|
||||
return await self._db_service.update_team(team)
|
||||
|
||||
async def add_member_to_team(self, team: Team, user: User, status: TeamStatus = TeamStatus.MEMBER) -> Team:
|
||||
if user in team.members:
|
||||
raise AlreadyMemberError()
|
||||
|
||||
await self._db_service.add_member_to_team(team, user, status)
|
||||
return await self.get_team_by_id(team.id)
|
||||
|
||||
async def remove_member_from_team(self, team: Team, user: User) -> Team:
|
||||
if user not in team.members:
|
||||
raise NotMemberError()
|
||||
|
||||
if team.members[user] == TeamStatus.LEADER:
|
||||
raise TeamLeadRemovalError()
|
||||
|
||||
await self._db_service.remove_user_from_team(team, user)
|
||||
return await self.get_team_by_id(team.id)
|
||||
|
||||
async def is_join_password_valid(self, team_id: int, join_password: str) -> bool:
|
||||
team = await self.get_team_by_id(team_id)
|
||||
if not team:
|
||||
return False
|
||||
return team.join_password == join_password
|
||||
|
||||
def _check_for_disallowed_char(self, name: str) -> Optional[str]:
|
||||
for c in name:
|
||||
if c not in self.ALLOWED_TEAM_NAME_SYMBOLS:
|
||||
return c
|
||||
return None
|
||||
@@ -1,20 +1,12 @@
|
||||
from asyncio import sleep
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
||||
from src.ezgg_lan_manager.services.UserService import UserService
|
||||
from src.ezgg_lan_manager.types.Participant import Participant
|
||||
from src.ezgg_lan_manager.types.Team import Team
|
||||
from src.ezgg_lan_manager.types.Tournament import Tournament
|
||||
from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, ParticipantType
|
||||
|
||||
DEV_LOREM_IPSUM = """
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Dieses LAN-Turnier bringt Spieler aus verschiedenen Regionen zusammen, um gemeinsam spannende Matches zu erleben. Tastaturen klappern, Monitore leuchten und die Stimmung ist von Anfang an von Vorfreude und Ehrgeiz geprägt.
|
||||
|
||||
Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. In intensiven Partien zählen Strategie, Reaktion und Teamarbeit. Zwischen den Spielen wird gefachsimpelt, gelacht und neue Kontakte entstehen, während Server stabil laufen und das Netzwerk dauerhaft gefordert ist.
|
||||
|
||||
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Wenn die Finalspiele beginnen, steigt die Spannung spürbar. Am Ende bleiben faire Wettkämpfe, gemeinsame Erinnerungen und das Gefühl, Teil einer starken Community gewesen zu sein.
|
||||
"""
|
||||
from src.ezgg_lan_manager.types.TournamentBase import ParticipantType, TournamentError
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
|
||||
class TournamentService:
|
||||
@@ -22,77 +14,88 @@ class TournamentService:
|
||||
self._db_service = db_service
|
||||
self._user_service = user_service
|
||||
|
||||
# This overrides the database access and is meant for easy development.
|
||||
# Set to None before merging back into main.
|
||||
self._dev_data = [
|
||||
Tournament(
|
||||
0,
|
||||
"Teeworlds 2vs2",
|
||||
DEV_LOREM_IPSUM,
|
||||
GameTitle(
|
||||
"Teeworlds",
|
||||
"Teeworlds is a free online multiplayer game, available for all major operating systems. Battle with up to 16 players in a variety of game modes.",
|
||||
"https://store.steampowered.com/app/380840/Teeworlds/",
|
||||
"teeworlds.png"
|
||||
),
|
||||
TournamentFormat.SINGLE_ELIMINATION_BO_3,
|
||||
datetime(2026, 5, 8, 18, 0, 0),
|
||||
TournamentStatus.OPEN,
|
||||
[],
|
||||
None,
|
||||
[],
|
||||
32
|
||||
),
|
||||
Tournament(
|
||||
1,
|
||||
"Rocket League 3vs3",
|
||||
DEV_LOREM_IPSUM,
|
||||
GameTitle(
|
||||
"Rocket League",
|
||||
"Rocket League is a high-powered hybrid of arcade-style soccer and vehicular mayhem with easy-to-understand controls and fluid, physics-driven competition.",
|
||||
"https://steamcommunity.com/app/252950",
|
||||
"rl.png"
|
||||
),
|
||||
TournamentFormat.SINGLE_ELIMINATION_BO_3,
|
||||
datetime(2026, 5, 8, 18, 0, 0),
|
||||
TournamentStatus.OPEN,
|
||||
[Participant(30, "Typhus", ParticipantType.PLAYER)],
|
||||
None,
|
||||
[],
|
||||
8
|
||||
),
|
||||
Tournament(
|
||||
2,
|
||||
"Worms Armageddon 1vs1",
|
||||
DEV_LOREM_IPSUM,
|
||||
GameTitle(
|
||||
"Worms Armageddon",
|
||||
"2D turn-based artillery strategy game.",
|
||||
"https://store.steampowered.com/app/217200/Worms_Armageddon/",
|
||||
"worms.png"
|
||||
),
|
||||
TournamentFormat.SINGLE_ELIMINATION_BO_1,
|
||||
datetime(2026, 5, 8, 18, 30, 0),
|
||||
TournamentStatus.CLOSED,
|
||||
[],
|
||||
None,
|
||||
[],
|
||||
16
|
||||
)
|
||||
]
|
||||
# Crude cache mechanism. If performance suffers, maybe implement a queue with Single-Owner-Pattern or a Lock
|
||||
self._cache: dict[int, Tournament] = {}
|
||||
self._cache_dirty: bool = True # Setting this flag invokes cache update on next read
|
||||
|
||||
async def _update_cache(self) -> None:
|
||||
tournaments = await self._db_service.get_all_tournaments()
|
||||
for tournament in tournaments:
|
||||
self._cache[tournament.id] = tournament
|
||||
self._cache_dirty = False
|
||||
|
||||
async def register_user_for_tournament(self, user_id: int, tournament_id: int) -> None:
|
||||
tournament = await self.get_tournament_by_id(tournament_id)
|
||||
if not tournament:
|
||||
raise TournamentError(f"No tournament with ID {tournament_id} was found")
|
||||
if tournament.participant_type != ParticipantType.PLAYER:
|
||||
raise TournamentError(f"Can only add single player to team tournament, not {tournament.participant_type.name}")
|
||||
participant = Participant(id_=user_id, participant_type=ParticipantType.PLAYER)
|
||||
tournament.add_participant(participant)
|
||||
await self._db_service.add_participant_to_tournament(participant, tournament)
|
||||
self._cache_dirty = True
|
||||
|
||||
async def register_team_for_tournament(self, team_id: int, tournament_id: int) -> None:
|
||||
tournament = await self.get_tournament_by_id(tournament_id)
|
||||
if not tournament:
|
||||
raise TournamentError(f"No tournament with ID {tournament_id} was found")
|
||||
if tournament.participant_type != ParticipantType.TEAM:
|
||||
raise TournamentError(f"Can only add team to team tournament, not {tournament.participant_type.name}")
|
||||
participant = Participant(id_=team_id, participant_type=ParticipantType.TEAM)
|
||||
tournament.add_participant(participant)
|
||||
await self._db_service.add_participant_to_tournament(participant, tournament)
|
||||
self._cache_dirty = True
|
||||
|
||||
async def unregister_user_from_tournament(self, user_id: int, tournament_id: int) -> None:
|
||||
tournament = await self.get_tournament_by_id(tournament_id)
|
||||
if not tournament:
|
||||
raise TournamentError(f"No tournament with ID {tournament_id} was found")
|
||||
participant = next(filter(lambda p: p.id == user_id, tournament.participants), None)
|
||||
if participant is not None:
|
||||
tournament.remove_participant(participant)
|
||||
await self._db_service.remove_participant_from_tournament(participant, tournament)
|
||||
self._cache_dirty = True
|
||||
|
||||
async def unregister_team_from_tournament(self, team_id: int, tournament_id: int) -> None:
|
||||
tournament = await self.get_tournament_by_id(tournament_id)
|
||||
if not tournament:
|
||||
raise TournamentError(f"No tournament with ID {tournament_id} was found")
|
||||
participant = next(filter(lambda p: p.id == team_id, tournament.participants), None)
|
||||
if participant is not None:
|
||||
tournament.remove_participant(participant)
|
||||
await self._db_service.remove_participant_from_tournament(participant, tournament)
|
||||
self._cache_dirty = True
|
||||
|
||||
async def get_tournaments(self) -> list[Tournament]:
|
||||
# Fake DB lookup delay
|
||||
await sleep(1)
|
||||
|
||||
if self._dev_data is not None:
|
||||
return self._dev_data
|
||||
return [] # ToDo: Implement database polling
|
||||
if self._cache_dirty:
|
||||
await self._update_cache()
|
||||
return list(self._cache.values())
|
||||
|
||||
async def get_tournament_by_id(self, tournament_id: int) -> Optional[Tournament]:
|
||||
await sleep(1)
|
||||
try:
|
||||
return self._dev_data[tournament_id]
|
||||
except IndexError:
|
||||
return None
|
||||
if self._cache_dirty:
|
||||
await self._update_cache()
|
||||
return self._cache.get(tournament_id, None)
|
||||
|
||||
async def get_users_from_participant_list(self, participants: list[Participant]) -> list[User]:
|
||||
all_users = await self._db_service.get_all_users()
|
||||
participant_ids = [p.id for p in participants]
|
||||
return list(filter(lambda u: u.user_id in participant_ids, all_users))
|
||||
|
||||
async def get_teams_from_participant_list(self, participants: list[Participant]) -> list[Team]:
|
||||
all_teams = await self._db_service.get_teams()
|
||||
participant_ids = [p.id for p in participants]
|
||||
return list(filter(lambda t: t.id in participant_ids, all_teams))
|
||||
|
||||
async def start_tournament(self, tournament_id: int):
|
||||
tournament = await self.get_tournament_by_id(tournament_id)
|
||||
if tournament:
|
||||
tournament.start()
|
||||
# ToDo: Write matches/round to database
|
||||
self._cache_dirty = True
|
||||
|
||||
async def cancel_tournament(self, tournament_id: int):
|
||||
tournament = await self.get_tournament_by_id(tournament_id)
|
||||
if tournament:
|
||||
tournament.cancel()
|
||||
# ToDo: Update to database
|
||||
self._cache_dirty = True
|
||||
|
||||
@@ -59,9 +59,12 @@ class UserService:
|
||||
|
||||
async def is_login_valid(self, user_name_or_mail: str, password_clear_text: str) -> bool:
|
||||
user = await self.get_user(user_name_or_mail)
|
||||
user_password_hash = sha256(password_clear_text.encode(encoding="utf-8")).hexdigest()
|
||||
if not user:
|
||||
return False
|
||||
return user.user_password == sha256(password_clear_text.encode(encoding="utf-8")).hexdigest()
|
||||
if user.user_fallback_password and user.user_fallback_password == user_password_hash:
|
||||
return True
|
||||
return user.user_password == user_password_hash
|
||||
|
||||
|
||||
def _check_for_disallowed_char(self, name: str) -> Optional[str]:
|
||||
|
||||
@@ -8,9 +8,9 @@ from src.ezgg_lan_manager.types.TournamentBase import MatchStatus, TournamentErr
|
||||
|
||||
|
||||
class MatchParticipant:
|
||||
def __init__(self, participant_id: int, slot_number: Literal[1, 2]) -> None:
|
||||
def __init__(self, participant_id: int, slot_number: Literal[-1, 1, 2]) -> None:
|
||||
self._participant_id = participant_id
|
||||
if slot_number not in (1, 2):
|
||||
if slot_number not in (-1, 1, 2):
|
||||
raise TournamentError("Invalid slot number")
|
||||
self.slot_number = slot_number
|
||||
|
||||
@@ -99,7 +99,9 @@ class Match:
|
||||
def next_match_lose_id(self) -> Optional[int]:
|
||||
return self._next_match_lose_id
|
||||
|
||||
def assign_participant(self, participant_id: int, slot: Literal[1, 2]) -> None:
|
||||
def assign_participant(self, participant_id: int, slot: Literal[-1, 1, 2]) -> None:
|
||||
if slot == -1:
|
||||
raise TournamentError("Normal match does not support slot -1")
|
||||
new_participant = MatchParticipant(participant_id, slot)
|
||||
if len(self._participants) < 2 and not any(p.participant_id == participant_id for p in self._participants):
|
||||
if len(self._participants) == 1 and self._participants[-1].slot_number == new_participant.slot_number:
|
||||
@@ -131,3 +133,28 @@ class Match:
|
||||
)
|
||||
return (f"<Match id={self._match_id} round={self._round_number} bracket={self._bracket.name} "
|
||||
f"status={self.status.name} participants=[{participants}] winner={self.winner}>")
|
||||
|
||||
class FFAMatch(Match):
|
||||
"""
|
||||
Specialized match that supports infinite participants
|
||||
"""
|
||||
def __init__(self, match_id: int, tournament_id: int, round_number: int, bracket: Bracket, best_of: int, status: MatchStatus,
|
||||
next_match_win_lose_ids: tuple[Optional[int], Optional[int]], match_has_ended_callback: Callable) -> None:
|
||||
super().__init__(match_id, tournament_id, round_number, bracket, best_of, status, next_match_win_lose_ids, match_has_ended_callback)
|
||||
|
||||
@property
|
||||
def is_fully_seeded(self) -> bool:
|
||||
return len(self._participants) > 1
|
||||
|
||||
def assign_participant(self, participant_id: int, slot: Literal[-1, 1, 2]) -> None:
|
||||
if slot != -1:
|
||||
raise TournamentError("FFAMatch does not support slot 1 and 2")
|
||||
new_participant = MatchParticipant(participant_id, slot)
|
||||
self._participants.append(new_participant)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
participants = ", ".join(
|
||||
f"{p.participant_id}" for p in self._participants
|
||||
)
|
||||
return (f"<Match id={self._match_id} round={self._round_number} bracket={self._bracket.name} "
|
||||
f"status={self.status.name} participants=[{participants}] winner={self.winner}>")
|
||||
|
||||
@@ -2,10 +2,9 @@ from src.ezgg_lan_manager.types.TournamentBase import ParticipantType
|
||||
|
||||
|
||||
class Participant:
|
||||
def __init__(self, id_: int, display_name: str, participant_type: ParticipantType) -> None:
|
||||
def __init__(self, id_: int, participant_type: ParticipantType) -> None:
|
||||
self._id = id_
|
||||
self._participant_type = participant_type
|
||||
self._display_name = display_name
|
||||
|
||||
@property
|
||||
def id(self) -> int:
|
||||
@@ -14,7 +13,3 @@ class Participant:
|
||||
@property
|
||||
def participant_type(self) -> ParticipantType:
|
||||
return self._participant_type
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
return self._display_name
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
|
||||
# ToDo: Persist between reloads: https://rio.dev/docs/howto/persistent-settings
|
||||
# Note for ToDo: rio.UserSettings are saved LOCALLY, do not just read a user_id here!
|
||||
@dataclass(frozen=False)
|
||||
class SessionStorage:
|
||||
_user_id: Optional[int] = None # DEBUG: Put user ID here to skip login
|
||||
_is_team_member: bool = False
|
||||
_notification_callbacks: dict[str, Callable] = field(default_factory=dict)
|
||||
|
||||
async def clear(self) -> None:
|
||||
await self.set_user_id_and_team_member_flag(None, False)
|
||||
|
||||
def subscribe_to_logged_in_or_out_event(self, component_id: str, callback: Callable) -> None:
|
||||
self._notification_callbacks[component_id] = callback
|
||||
|
||||
@property
|
||||
def user_id(self) -> Optional[int]:
|
||||
return self._user_id
|
||||
|
||||
@property
|
||||
def is_team_member(self) -> bool:
|
||||
return self._is_team_member
|
||||
|
||||
async def set_user_id_and_team_member_flag(self, user_id: Optional[int], is_team_member: bool) -> None:
|
||||
self._user_id = user_id
|
||||
self._is_team_member = is_team_member
|
||||
for component_id, callback in self._notification_callbacks.items():
|
||||
logger.debug(f"Calling logged in callback from {component_id}")
|
||||
await callback()
|
||||
@@ -0,0 +1,37 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Self
|
||||
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
class TeamStatus(Enum):
|
||||
MEMBER = 0
|
||||
OFFICER = 1
|
||||
LEADER = 2
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, team_status: str) -> Self:
|
||||
if team_status == "MEMBER":
|
||||
return TeamStatus.MEMBER
|
||||
elif team_status == "OFFICER":
|
||||
return TeamStatus.OFFICER
|
||||
elif team_status == "LEADER":
|
||||
return TeamStatus.LEADER
|
||||
raise ValueError
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Team:
|
||||
id: int
|
||||
name: str
|
||||
abbreviation: str
|
||||
members: dict[User, TeamStatus]
|
||||
join_password: str
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.id)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Team):
|
||||
return NotImplemented
|
||||
return self.id == other.id
|
||||
@@ -3,9 +3,9 @@ from datetime import datetime
|
||||
from typing import Optional
|
||||
from math import ceil, log2
|
||||
|
||||
from src.ezgg_lan_manager.types.Match import Match
|
||||
from src.ezgg_lan_manager.types.Match import Match, FFAMatch
|
||||
from src.ezgg_lan_manager.types.Participant import Participant
|
||||
from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, TournamentError, Bracket, MatchStatus
|
||||
from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, TournamentError, Bracket, MatchStatus, ParticipantType
|
||||
|
||||
|
||||
class Tournament:
|
||||
@@ -20,7 +20,8 @@ class Tournament:
|
||||
participants: list[Participant],
|
||||
matches: Optional[tuple[Match]],
|
||||
rounds: list[list[Match]],
|
||||
max_participants: int) -> None:
|
||||
max_participants: int,
|
||||
participant_type: ParticipantType) -> None:
|
||||
self._id = id_
|
||||
self._name = name
|
||||
self._description = description
|
||||
@@ -32,6 +33,7 @@ class Tournament:
|
||||
self._matches = matches
|
||||
self._rounds = rounds
|
||||
self._max_participants = max_participants
|
||||
self._participant_type = participant_type
|
||||
|
||||
@property
|
||||
def id(self) -> int:
|
||||
@@ -85,11 +87,23 @@ class Tournament:
|
||||
def is_full(self) -> bool:
|
||||
return len(self._participants) >= self._max_participants
|
||||
|
||||
@property
|
||||
def participant_type(self) -> ParticipantType:
|
||||
return self._participant_type
|
||||
|
||||
def add_participant(self, participant: Participant) -> None:
|
||||
if participant.id in (p.id for p in self._participants):
|
||||
raise TournamentError(f"Participant with ID {participant.id} already registered for tournament")
|
||||
self._participants.append(participant)
|
||||
|
||||
def remove_participant(self, participant: Participant) -> None:
|
||||
if participant.id not in (p.id for p in self._participants):
|
||||
raise TournamentError(f"Participant with ID {participant.id} not registered for tournament")
|
||||
# ToDo: Check if tournament already started => correctly resolve matches with now missing participant
|
||||
self._participants.remove(participant)
|
||||
|
||||
def cancel(self):
|
||||
self.status = TournamentStatus.CANCELED
|
||||
|
||||
def match_has_ended_callback(self, match: Match) -> None:
|
||||
if self._matches is None:
|
||||
@@ -124,10 +138,12 @@ class Tournament:
|
||||
bracket = "SINGLE"
|
||||
elif fmt.name.startswith("DOUBLE_ELIMINATION"):
|
||||
bracket = "DOUBLE"
|
||||
elif fmt.name.startswith("FFA"):
|
||||
bracket = "FINAL"
|
||||
else:
|
||||
raise TournamentError(f"Unsupported tournament format: {fmt}")
|
||||
|
||||
if fmt.name.endswith("_BO_1"):
|
||||
if fmt.name.endswith("_BO_1") or fmt.name.endswith("FFA"):
|
||||
bo = 1
|
||||
elif fmt.name.endswith("_BO_3"):
|
||||
bo = 3
|
||||
@@ -145,7 +161,28 @@ class Tournament:
|
||||
num_participants = len(self.participants)
|
||||
match_id_counter = 1
|
||||
|
||||
if bracket_type == "SINGLE":
|
||||
if bracket_type == "FINAL":
|
||||
rounds: list[list[Match]] = []
|
||||
round_matches = []
|
||||
match = FFAMatch(
|
||||
match_id=match_id_counter,
|
||||
tournament_id=self._id,
|
||||
round_number=1,
|
||||
bracket=Bracket.FINAL,
|
||||
best_of=best_of,
|
||||
status=MatchStatus.WAITING,
|
||||
next_match_win_lose_ids=(None, None),
|
||||
match_has_ended_callback=self.match_has_ended_callback
|
||||
)
|
||||
|
||||
for participant in self.participants:
|
||||
match.assign_participant(participant.id, -1)
|
||||
|
||||
round_matches.append(match)
|
||||
rounds.append(round_matches)
|
||||
self._matches = [match]
|
||||
|
||||
elif bracket_type == "SINGLE":
|
||||
# --- single-elimination as before ---
|
||||
num_rounds = ceil(log2(num_participants))
|
||||
rounds: list[list[Match]] = []
|
||||
|
||||
@@ -16,9 +16,10 @@ class TournamentFormat(Enum):
|
||||
DOUBLE_ELIMINATION_BO_1 = 4
|
||||
DOUBLE_ELIMINATION_BO_3 = 5
|
||||
DOUBLE_ELIMINATION_BO_5 = 6
|
||||
FFA = 7
|
||||
|
||||
def tournament_format_to_display_texts(tournament_format: TournamentFormat) -> tuple[str, str]:
|
||||
""" Returns tuple where idx 0 is SE/DE string and idx 1 is match count """
|
||||
""" Returns tuple where idx 0 is SE/DE/FFA string and idx 1 is match count """
|
||||
if tournament_format == TournamentFormat.SINGLE_ELIMINATION_BO_1:
|
||||
return "Single Elimination", "1"
|
||||
elif tournament_format == TournamentFormat.SINGLE_ELIMINATION_BO_3:
|
||||
@@ -31,6 +32,8 @@ def tournament_format_to_display_texts(tournament_format: TournamentFormat) -> t
|
||||
return "Double Elimination", "3"
|
||||
elif tournament_format == TournamentFormat.DOUBLE_ELIMINATION_BO_5:
|
||||
return "Double Elimination", "5"
|
||||
elif tournament_format == TournamentFormat.FFA:
|
||||
return "Free for All", "1"
|
||||
else:
|
||||
raise RuntimeError(f"Unknown tournament status: {str(tournament_format)}")
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ class User:
|
||||
user_name: str
|
||||
user_mail: str
|
||||
user_password: str
|
||||
user_fallback_password: Optional[str]
|
||||
user_first_name: Optional[str]
|
||||
user_last_name: Optional[str]
|
||||
user_birth_day: Optional[date]
|
||||
@@ -19,4 +20,9 @@ class User:
|
||||
last_updated_at: datetime
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(f"{self.user_id}{self.user_name}{self.user_mail}")
|
||||
return hash(self.user_id)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, User):
|
||||
return NotImplemented
|
||||
return self.user_id == other.user_id
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserSession:
|
||||
id: UUID
|
||||
user_id: int
|
||||
is_team_member: bool
|
||||
@@ -16,9 +16,9 @@ class TournamentDomainTests(unittest.TestCase):
|
||||
self.initial_status = TournamentStatus.CLOSED
|
||||
|
||||
# Generic Participants
|
||||
self.participant_a = Participant(1, "CoolUserName", ParticipantType.PLAYER)
|
||||
self.participant_b = Participant(2, "CrazyUserName", ParticipantType.PLAYER)
|
||||
self.participant_c = Participant(3, "FunnyUserName", ParticipantType.PLAYER)
|
||||
self.participant_a = Participant(1, ParticipantType.PLAYER)
|
||||
self.participant_b = Participant(2, ParticipantType.PLAYER)
|
||||
self.participant_c = Participant(3, ParticipantType.PLAYER)
|
||||
|
||||
def test_tournament_without_participants_can_not_be_started(self) -> None:
|
||||
tournament_under_test = generate_new_tournament(self.name, self.description, self.game_title, self.format_, self.start_time, 32, self.initial_status)
|
||||
@@ -64,4 +64,19 @@ class TournamentDomainTests(unittest.TestCase):
|
||||
self.assertEqual(sm.status, MatchStatus.WAITING)
|
||||
self.assertEqual(sm.participants[0].participant_id, self.participant_c.id)
|
||||
self.assertEqual(sm.participants[0].slot_number, 1)
|
||||
self.assertIsNone(sm.winner)
|
||||
self.assertIsNone(sm.winner)
|
||||
|
||||
def test_ffa_tournament_with_15_participants_gets_generated_correctly(self) -> None:
|
||||
tournament_under_test = generate_new_tournament("Among Us", "It's Among Us", GameTitle("Among Us", "", "", ""), TournamentFormat.FFA, self.start_time, 32, TournamentStatus.OPEN)
|
||||
|
||||
for i in range(1, 16):
|
||||
tournament_under_test.add_participant(Participant(i, ParticipantType.PLAYER))
|
||||
tournament_under_test.start()
|
||||
|
||||
# Assert Tournament was switched to ONGOING
|
||||
self.assertEqual(TournamentStatus.ONGOING, tournament_under_test.status)
|
||||
|
||||
matches_in_tournament = sorted(tournament_under_test.matches, key=lambda m: m.match_id)
|
||||
|
||||
self.assertEqual(1, len(matches_in_tournament))
|
||||
self.assertEqual(15, len(matches_in_tournament[0].participants))
|
||||
|
||||