Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fffb607b16 | |||
| 02658aa049 | |||
| 1137a9e7c7 | |||
| b6ef2b5995 |
@@ -8,16 +8,13 @@ This repository contains the code for the EZGG LAN Manager.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Working Installation of MariaDB Server (version `10.6.25` or later)
|
||||
+ MySQL should work too, but there are no guarantees.
|
||||
- Working Installation of MySQL 5 or latest MariaDB Server (`mariadb-server` for Debian-based Linux, `XAMPP` for Windows)
|
||||
- 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 piping the file into the mariadb-server executable.
|
||||
|
||||
After creating the database, apply all patches found in `sql/*_patch.sql` in their numeric order.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -46,4 +43,3 @@ 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", "-pAlkohol1"]
|
||||
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
@@ -30,8 +30,6 @@ services:
|
||||
volumes:
|
||||
- database:/var/lib/mysql
|
||||
- ./sql/create_database.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
- ./sql:/sql
|
||||
- ./tournament_data:/opt/ezgg-lan-manager/tournament_data
|
||||
|
||||
|
||||
volumes:
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
-- =====================================================
|
||||
-- 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);
|
||||
@@ -1,10 +0,0 @@
|
||||
-- =====================================================
|
||||
-- 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;
|
||||
@@ -1,5 +0,0 @@
|
||||
-- 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,5 +1,5 @@
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
from asyncio import get_event_loop
|
||||
|
||||
import sys
|
||||
|
||||
@@ -8,10 +8,11 @@ from pathlib import Path
|
||||
from rio import App, Theme, Color, Font, ComponentPage, Session
|
||||
from from_root import from_root
|
||||
|
||||
from src.ezgg_lan_manager import pages, init_services, LocalDataService, RefreshService
|
||||
from src.ezgg_lan_manager import pages, init_services
|
||||
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.UserSession import UserSession
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
|
||||
logger = logging.getLogger("EzggLanManager")
|
||||
|
||||
@@ -29,20 +30,14 @@ if __name__ == "__main__":
|
||||
corner_radius_large=0,
|
||||
font=Font(from_root("src/ezgg_lan_manager/assets/fonts/joystix.otf"))
|
||||
)
|
||||
default_attachments: list = [LocalData(stored_session_token=None)]
|
||||
default_attachments: list = [LocalData()]
|
||||
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(RefreshService())
|
||||
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)
|
||||
session.attach(SessionStorage())
|
||||
|
||||
async def on_app_start(a: App) -> None:
|
||||
init_result = await a.default_attachments[4].init_db_pool()
|
||||
@@ -156,24 +151,12 @@ if __name__ == "__main__":
|
||||
build=pages.ManageCateringPage,
|
||||
guard=team_guard
|
||||
),
|
||||
ComponentPage(
|
||||
name="NewPosOrderPage",
|
||||
url_segment="new-pos-order",
|
||||
build=pages.NewPosOrderPage,
|
||||
guard=team_guard
|
||||
),
|
||||
ComponentPage(
|
||||
name="ManageTournamentsPage",
|
||||
url_segment="manage-tournaments",
|
||||
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",
|
||||
@@ -184,25 +167,10 @@ if __name__ == "__main__":
|
||||
url_segment="tournament",
|
||||
build=pages.TournamentDetailsPage,
|
||||
),
|
||||
ComponentPage(
|
||||
name="TournamentTreePage",
|
||||
url_segment="tournament-tree",
|
||||
build=pages.TournamentTreePage,
|
||||
),
|
||||
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,
|
||||
@@ -228,14 +196,7 @@ if __name__ == "__main__":
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
app.run_as_web_server(
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
logger.info("EZGG LAN Manager was shut down.")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
sys.exit(1)
|
||||
sys.exit(app.run_as_web_server(
|
||||
host="0.0.0.0",
|
||||
port=8001,
|
||||
))
|
||||
|
||||
@@ -10,17 +10,15 @@ 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, TeamService]:
|
||||
def init_services() -> tuple[AccountingService, CateringService, ConfigurationService, DatabaseService, MailingService, NewsService, SeatingService, TicketingService, UserService, LocalDataService, ReceiptPrintingService, TournamentService]:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
configuration_service = ConfigurationService(from_root("config.toml"))
|
||||
db_service = DatabaseService(configuration_service.get_database_configuration())
|
||||
@@ -34,7 +32,6 @@ 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, team_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
|
||||
|
||||
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 44 KiB |
@@ -1,36 +0,0 @@
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
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,30 +1,29 @@
|
||||
import logging
|
||||
from asyncio import create_task, sleep
|
||||
from functools import partial
|
||||
from typing import Optional, Callable
|
||||
|
||||
from rio import Component, Row, Card, Column, Text, TextStyle, Spacer, PointerEventListener, Button, Rectangle, Popup, Icon, Color
|
||||
from rio import Component, Row, Card, Column, Text, TextStyle, Spacer, PointerEventListener, Button
|
||||
|
||||
from src.ezgg_lan_manager import ReceiptPrintingService
|
||||
from src.ezgg_lan_manager.components.StatusChangePopup import StatusChangePopup
|
||||
from src.ezgg_lan_manager.services.CateringService import CateringService
|
||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus
|
||||
from src.ezgg_lan_manager.types.Seat import Seat
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
class CateringManagementOrderDisplayStatusButton(Component):
|
||||
status: CateringOrderStatus
|
||||
clicked_cb: Callable
|
||||
def build(self) -> Component:
|
||||
return Button(
|
||||
content=Text(
|
||||
CateringOrder.translate_order_status(self.status)
|
||||
),
|
||||
shape="rectangle",
|
||||
on_press=partial(self.clicked_cb, self.status)
|
||||
)
|
||||
|
||||
|
||||
class CateringManagementOrderDisplay(Component):
|
||||
order: CateringOrder
|
||||
seat: Optional[Seat]
|
||||
clicked_cb: Callable
|
||||
status_change_popup_open: bool = False
|
||||
|
||||
def reprint_order(self) -> None:
|
||||
create_task(self.session[ReceiptPrintingService].print_order(self.order.customer, self.order))
|
||||
|
||||
def open_status_change_popup(self) -> None:
|
||||
self.status_change_popup_open = True
|
||||
|
||||
def format_order_status(self, status: CateringOrderStatus) -> Text:
|
||||
status_text = CateringOrder.translate_order_status(status)
|
||||
@@ -37,12 +36,14 @@ class CateringManagementOrderDisplay(Component):
|
||||
|
||||
return Text(text=status_text, style=TextStyle(fill=color))
|
||||
|
||||
async def change_status(self, new_status: CateringOrderStatus) -> Optional[str]:
|
||||
await sleep(1)
|
||||
async def status_button_clicked(self, new_status: CateringOrderStatus) -> None:
|
||||
if self.order.status == CateringOrderStatus.CANCELED:
|
||||
return
|
||||
|
||||
logger.debug(f"Status of order with ID {self.order.order_id} changing from {self.order.status} to {new_status}")
|
||||
if self.order.status == CateringOrderStatus.CANCELED: # Can not un-cancel
|
||||
return "Stornierte Bestellungen können nicht angepasst werden"
|
||||
if new_status == CateringOrderStatus.CANCELED:
|
||||
# ToDo: Hier sollten wir nochmal nachfragen ob der Bediener sich wirklich sicher ist,
|
||||
# und anwarnen das eine stornierte Bestellung nicht ent-storniert werden kann.
|
||||
pass
|
||||
|
||||
if self.order.status != new_status:
|
||||
if new_status == CateringOrderStatus.CANCELED:
|
||||
@@ -60,58 +61,43 @@ class CateringManagementOrderDisplay(Component):
|
||||
is_delivery=self.order.is_delivery
|
||||
)
|
||||
|
||||
self.status_change_popup_open = False
|
||||
|
||||
|
||||
def build(self) -> Component:
|
||||
card = Card(
|
||||
content=Column(
|
||||
Row(
|
||||
Text(f"ID: {self.order.order_id}", margin_left=0.3, margin_top=0.2, justify="center", style=TextStyle(font_size=1.2)),
|
||||
),
|
||||
Row(
|
||||
Text(f"Status: ", margin_left=0.3, margin_top=0.2),
|
||||
self.format_order_status(self.order.status),
|
||||
Spacer(),
|
||||
Text(self.order.order_date.strftime("%d.%m. - %H:%M Uhr"), margin_right=0.3),
|
||||
),
|
||||
Row(
|
||||
Text(f"Gast: {self.order.customer.user_name}", margin_left=0.3),
|
||||
Spacer(),
|
||||
Text(f"Sitzplatz: {'-' if not self.seat else self.seat.seat_id}", margin_right=0.3),
|
||||
),
|
||||
Row(
|
||||
Text("Diese Bestellung wird:", margin_left=0.3, margin_bottom=0.5),
|
||||
Spacer(),
|
||||
Text("Geliefert" if self.order.is_delivery else "Abgeholt", margin_right=0.3, margin_bottom=0.5),
|
||||
),
|
||||
Row(
|
||||
Rectangle(
|
||||
content=Button(
|
||||
content=Text("Beleg drucken", justify="left"),
|
||||
shape="rectangle",
|
||||
on_press=self.reprint_order
|
||||
),
|
||||
stroke_width=0.1
|
||||
),
|
||||
Rectangle(
|
||||
content=Button(
|
||||
content=Text("Status ändern", justify="right"),
|
||||
shape="rectangle",
|
||||
on_press=self.open_status_change_popup
|
||||
),
|
||||
stroke_width=0.1
|
||||
),
|
||||
)
|
||||
),
|
||||
color=self.session.theme.hud_color,
|
||||
colorize_on_hover=True,
|
||||
margin=1
|
||||
)
|
||||
|
||||
status_change_popup = StatusChangePopup(card, self.status_change_popup_open, self.change_status)
|
||||
|
||||
return PointerEventListener(
|
||||
content=status_change_popup,
|
||||
content=Card(
|
||||
content=Column(
|
||||
Row(
|
||||
Text(f"ID: {self.order.order_id}", margin_left=0.3, margin_top=0.2, justify="center", style=TextStyle(font_size=1.2)),
|
||||
),
|
||||
Row(
|
||||
Text(f"Status: ", margin_left=0.3, margin_top=0.2),
|
||||
self.format_order_status(self.order.status),
|
||||
Spacer(),
|
||||
Text(self.order.order_date.strftime("%d.%m. - %H:%M Uhr"), margin_right=0.3),
|
||||
),
|
||||
Row(
|
||||
Text(f"Gast: {self.order.customer.user_name}", margin_left=0.3),
|
||||
Spacer(),
|
||||
Text(f"Sitzplatz: {'-' if not self.seat else self.seat.seat_id}", margin_right=0.3),
|
||||
),
|
||||
Row(
|
||||
Text("Diese Bestellung wird:", margin_left=0.3, margin_bottom=0.5),
|
||||
Spacer(),
|
||||
Text("Geliefert" if self.order.is_delivery else "Abgeholt", margin_right=0.3, margin_bottom=0.5),
|
||||
),
|
||||
Row(
|
||||
CateringManagementOrderDisplayStatusButton(CateringOrderStatus.RECEIVED, self.status_button_clicked),
|
||||
CateringManagementOrderDisplayStatusButton(CateringOrderStatus.CANCELED, self.status_button_clicked),
|
||||
CateringManagementOrderDisplayStatusButton(CateringOrderStatus.EN_ROUTE, self.status_button_clicked)
|
||||
),
|
||||
Row(
|
||||
CateringManagementOrderDisplayStatusButton(CateringOrderStatus.READY_FOR_PICKUP, self.status_button_clicked),
|
||||
CateringManagementOrderDisplayStatusButton(CateringOrderStatus.COMPLETED, self.status_button_clicked),
|
||||
CateringManagementOrderDisplayStatusButton(CateringOrderStatus.DELAYED, self.status_button_clicked),
|
||||
)
|
||||
),
|
||||
color=self.session.theme.hud_color,
|
||||
colorize_on_hover=True,
|
||||
margin=1
|
||||
),
|
||||
on_press=partial(self.clicked_cb, self.order)
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import Callable
|
||||
|
||||
from rio import Component, Row, Text, TextStyle, Color, Rectangle, PointerEventListener
|
||||
from rio import Component, Row, Text, TextStyle, Color, Rectangle, CursorStyle
|
||||
from rio.components.pointer_event_listener import PointerEvent, PointerEventListener
|
||||
|
||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrderStatus, CateringOrder
|
||||
|
||||
@@ -40,7 +41,7 @@ class CateringOrderItem(Component):
|
||||
fill=self.session.theme.primary_color,
|
||||
hover_fill=self.session.theme.hud_color,
|
||||
transition_time=0.1,
|
||||
cursor="pointer"
|
||||
cursor=CursorStyle.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" if self.is_sensitive else "material/do_not_disturb_on_total_silence",
|
||||
icon="material/add",
|
||||
min_size=2,
|
||||
color=self.session.theme.success_color if self.is_sensitive else self.session.theme.danger_color,
|
||||
style="colored-text",
|
||||
color=self.session.theme.success_color,
|
||||
style="plain-text",
|
||||
on_press=lambda: self.on_add_callback(self.article_id),
|
||||
is_sensitive=self.is_sensitive
|
||||
),
|
||||
|
||||
@@ -1,58 +1,88 @@
|
||||
from copy import copy, deepcopy
|
||||
from typing import Optional, Callable
|
||||
|
||||
from rio import Component, event, Spacer, Card, Column, Text, TextStyle
|
||||
from rio import *
|
||||
|
||||
from src.ezgg_lan_manager.services.ConfigurationService import ConfigurationService
|
||||
from src.ezgg_lan_manager.services.UserService import UserService
|
||||
from src.ezgg_lan_manager import ConfigurationService, UserService, LocalDataService
|
||||
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 on_populate(self) -> None:
|
||||
try:
|
||||
self.user = await self.session[UserService].get_user(self.session[UserSession].user_id)
|
||||
except KeyError:
|
||||
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:
|
||||
self.user = None
|
||||
|
||||
def build(self) -> Component:
|
||||
lan_info = self.session[ConfigurationService].get_lan_info()
|
||||
user_info_and_login_box = UserInfoAndLoginBox(state_changed_cb=self.on_populate)
|
||||
navigation = [
|
||||
user_info_and_login_box = UserInfoAndLoginBox()
|
||||
self.force_login_box_refresh.append(user_info_and_login_box.force_refresh)
|
||||
user_navigation = [
|
||||
DesktopNavigationButton("News", "./news"),
|
||||
Spacer(min_height=0.7),
|
||||
Spacer(min_height=1),
|
||||
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=0.7),
|
||||
Spacer(min_height=1),
|
||||
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),
|
||||
Spacer(min_height=0.7)
|
||||
DesktopNavigationButton("Kontakt", "./contact"),
|
||||
DesktopNavigationButton("Impressum & DSGVO", "./imprint"),
|
||||
Spacer(min_height=1)
|
||||
]
|
||||
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 []
|
||||
|
||||
if self.user is not None and self.user.is_team_member:
|
||||
navigation.insert(0, DesktopNavigationButton("Adminbereich", "./admin", is_team_navigation=True))
|
||||
nav_to_use = copy(team_navigation) if self.user is not None and self.user.is_team_member else copy(user_navigation)
|
||||
|
||||
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,
|
||||
*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/"),
|
||||
*nav_to_use,
|
||||
align_y=0
|
||||
),
|
||||
color=self.session.theme.neutral_color,
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import uuid
|
||||
|
||||
from rio import Component, TextStyle, Color, TextInput, Button, Text, Rectangle, Column, Row, Spacer, \
|
||||
EventHandler, Webview
|
||||
EventHandler
|
||||
|
||||
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):
|
||||
@@ -29,13 +26,11 @@ class LoginBox(Component):
|
||||
self.password_input_is_valid = True
|
||||
self.login_button_is_loading = False
|
||||
self.is_account_locked = False
|
||||
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)
|
||||
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])
|
||||
self.session[LocalData].stored_session_token = token
|
||||
self.session.attach(self.session[LocalData])
|
||||
await self.status_change_cb()
|
||||
await self.session[RefreshService].trigger_refresh()
|
||||
self.status_change_cb()
|
||||
else:
|
||||
self.user_name_input_is_valid = False
|
||||
self.password_input_is_valid = False
|
||||
@@ -107,5 +102,5 @@ class LoginBox(Component):
|
||||
min_width=12,
|
||||
align_x=0.5,
|
||||
margin_top=0.3,
|
||||
margin_bottom=1.5
|
||||
)
|
||||
margin_bottom=2
|
||||
)
|
||||
@@ -1,23 +0,0 @@
|
||||
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,10 +1,11 @@
|
||||
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.UserSession import UserSession
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
|
||||
|
||||
class SeatingPlanInfoBox(Component):
|
||||
@@ -21,14 +22,11 @@ class SeatingPlanInfoBox(Component):
|
||||
|
||||
@event.on_populate
|
||||
async def check_ticket(self) -> None:
|
||||
try:
|
||||
user_id = self.session[UserSession].user_id
|
||||
user_ticket = await self.session[TicketingService].get_user_ticket(user_id)
|
||||
if self.session[SessionStorage].user_id:
|
||||
user_ticket = await self.session[TicketingService].get_user_ticket(self.session[SessionStorage].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:
|
||||
@@ -37,11 +35,6 @@ 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",
|
||||
@@ -82,9 +75,9 @@ class SeatingPlanInfoBox(Component):
|
||||
grow_y=False,
|
||||
is_sensitive=not self.is_booking_blocked,
|
||||
on_press=self.purchase_clicked
|
||||
) 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"),
|
||||
) 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"),
|
||||
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.UserSession import UserSession
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
|
||||
|
||||
class SeatPixel(Component):
|
||||
@@ -14,11 +14,7 @@ class SeatPixel(Component):
|
||||
seat_orientation: Literal["top", "bottom"]
|
||||
|
||||
def determine_color(self) -> Color:
|
||||
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:
|
||||
if self.seat.user is not None and self.seat.user.user_id == self.session[SessionStorage].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
|
||||
|
||||
from rio import Component, Column, Text, TextStyle, Button, Row, ScrollContainer, Spacer, Popup, Table, event, Card
|
||||
import rio
|
||||
from rio import Component, Column, Text, TextStyle, Button, Row, ScrollContainer, Spacer, Popup, Table, event
|
||||
|
||||
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.UserSession import UserSession
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
|
||||
POPUP_CLOSE_TIMEOUT_SECONDS = 3
|
||||
|
||||
@@ -23,21 +23,16 @@ class ShoppingCartAndOrders(Component):
|
||||
|
||||
@event.periodic(5)
|
||||
async def periodic_refresh_of_orders(self) -> None:
|
||||
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)
|
||||
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)
|
||||
|
||||
async def switch(self) -> None:
|
||||
self.show_cart = not self.show_cart
|
||||
user_id = self._get_user_id()
|
||||
if user_id is not None:
|
||||
self.orders = await self.session[CateringService].get_orders_for_user(user_id)
|
||||
self.orders = await self.session[CateringService].get_orders_for_user(self.session[SessionStorage].user_id)
|
||||
|
||||
async def on_remove_item(self, list_id: int) -> None:
|
||||
catering_service = self.session[CateringService]
|
||||
user_id = self._get_user_id()
|
||||
if user_id is None:
|
||||
return
|
||||
user_id = self.session[SessionStorage].user_id
|
||||
cart = catering_service.get_cart(user_id)
|
||||
try:
|
||||
cart.pop(list_id)
|
||||
@@ -47,16 +42,13 @@ class ShoppingCartAndOrders(Component):
|
||||
self.force_refresh()
|
||||
|
||||
async def on_empty_cart_pressed(self) -> None:
|
||||
user_id = self._get_user_id()
|
||||
if user_id is None:
|
||||
return
|
||||
self.session[CateringService].save_cart(user_id, [])
|
||||
self.session[CateringService].save_cart(self.session[SessionStorage].user_id, [])
|
||||
self.force_refresh()
|
||||
|
||||
async def on_add_item(self, article_id: int) -> None:
|
||||
catering_service = self.session[CateringService]
|
||||
user_id = self._get_user_id()
|
||||
if user_id is None:
|
||||
user_id = self.session[SessionStorage].user_id
|
||||
if not user_id:
|
||||
return
|
||||
cart = catering_service.get_cart(user_id)
|
||||
item_to_add = await catering_service.get_menu_item_by_id(article_id)
|
||||
@@ -77,9 +69,7 @@ class ShoppingCartAndOrders(Component):
|
||||
self.order_button_loading = True
|
||||
self.force_refresh()
|
||||
|
||||
user_id = self._get_user_id()
|
||||
if user_id is None:
|
||||
return
|
||||
user_id = self.session[SessionStorage].user_id
|
||||
cart = self.session[CateringService].get_cart(user_id)
|
||||
show_popup_task = None
|
||||
if len(cart) < 1:
|
||||
@@ -101,13 +91,13 @@ class ShoppingCartAndOrders(Component):
|
||||
else:
|
||||
show_popup_task = create_task(self.show_popup("Unbekannter Fehler", True))
|
||||
else:
|
||||
self.session[CateringService].save_cart(user_id, [])
|
||||
self.session[CateringService].save_cart(self.session[SessionStorage].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() -> Component:
|
||||
def build_dialog_content() -> rio.Component:
|
||||
# @todo: rio 0.10.8 did not have the ability to align the columns, check back in a future version
|
||||
table = Table(
|
||||
{
|
||||
@@ -117,9 +107,9 @@ class ShoppingCartAndOrders(Component):
|
||||
},
|
||||
show_row_numbers=False
|
||||
)
|
||||
return Card(
|
||||
Column(
|
||||
Text(
|
||||
return rio.Card(
|
||||
rio.Column(
|
||||
rio.Text(
|
||||
f"Deine Bestellung ({order.order_id})",
|
||||
align_x=0.5,
|
||||
margin_bottom=0.5
|
||||
@@ -144,16 +134,10 @@ class ShoppingCartAndOrders(Component):
|
||||
)
|
||||
await dialog.wait_for_close()
|
||||
|
||||
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()
|
||||
def build(self) -> rio.Component:
|
||||
user_id = self.session[SessionStorage].user_id
|
||||
catering_service = self.session[CateringService]
|
||||
cart = catering_service.get_cart(user_id) if user_id is not None else []
|
||||
cart = catering_service.get_cart(user_id)
|
||||
if self.show_cart:
|
||||
cart_container = ScrollContainer(
|
||||
content=Column(
|
||||
@@ -171,6 +155,7 @@ 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),
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
from functools import partial
|
||||
from typing import Callable, Optional
|
||||
|
||||
from rio import Column, Row, Text, Button, Component, Icon, Popup, Rectangle, Color, Tooltip, PointerEventListener, PointerEvent, ProgressCircle
|
||||
|
||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrderStatus, CateringOrder
|
||||
|
||||
ICONS_BY_STATUS = {
|
||||
CateringOrderStatus.RECEIVED: "material/move_to_inbox",
|
||||
CateringOrderStatus.DELAYED: "material/hourglass_top",
|
||||
CateringOrderStatus.READY_FOR_PICKUP: "material/takeout_dining",
|
||||
CateringOrderStatus.EN_ROUTE: "material/local_shipping",
|
||||
CateringOrderStatus.COMPLETED: "material/check_circle",
|
||||
CateringOrderStatus.CANCELED: "material/cancel",
|
||||
}
|
||||
|
||||
|
||||
class StatusChangeButton(Component):
|
||||
status: CateringOrderStatus
|
||||
clicked_cb: Callable
|
||||
|
||||
def build(self) -> Component:
|
||||
return Tooltip(
|
||||
anchor=PointerEventListener(
|
||||
content=Rectangle(
|
||||
fill=Color.TRANSPARENT,
|
||||
content=Column(
|
||||
Icon(
|
||||
icon=ICONS_BY_STATUS[self.status]
|
||||
)
|
||||
),
|
||||
stroke_width=0.1,
|
||||
stroke_color=Color.TRANSPARENT,
|
||||
hover_stroke_width=0.1,
|
||||
hover_stroke_color=Color.BLACK
|
||||
),
|
||||
on_press=partial(self.clicked_cb, self.status)
|
||||
),
|
||||
tip=Text(text=CateringOrder.translate_order_status(self.status)),
|
||||
position="top"
|
||||
)
|
||||
|
||||
|
||||
class StatusChangePopup(Component):
|
||||
anchor: Component
|
||||
popup_open: bool
|
||||
status_should_change_cb: Callable
|
||||
response: Optional[str] = None
|
||||
is_loading: bool = False
|
||||
|
||||
async def handle_button_clicked(self, status: CateringOrderStatus, _: PointerEvent) -> None:
|
||||
self.is_loading = True
|
||||
self.response = await self.status_should_change_cb(status)
|
||||
self.is_loading = False
|
||||
|
||||
def close(self) -> None:
|
||||
self.popup_open = False
|
||||
|
||||
def build(self) -> Component:
|
||||
if self.is_loading:
|
||||
content = Row(
|
||||
ProgressCircle(margin=1)
|
||||
)
|
||||
elif self.response:
|
||||
content = Row(
|
||||
Text(text=self.response, justify="center", overflow="wrap", margin=1)
|
||||
)
|
||||
else:
|
||||
content = Row(
|
||||
StatusChangeButton(CateringOrderStatus.RECEIVED, self.handle_button_clicked),
|
||||
StatusChangeButton(CateringOrderStatus.DELAYED, self.handle_button_clicked),
|
||||
StatusChangeButton(CateringOrderStatus.READY_FOR_PICKUP, self.handle_button_clicked),
|
||||
StatusChangeButton(CateringOrderStatus.EN_ROUTE, self.handle_button_clicked),
|
||||
StatusChangeButton(CateringOrderStatus.COMPLETED, self.handle_button_clicked),
|
||||
StatusChangeButton(CateringOrderStatus.CANCELED, self.handle_button_clicked),
|
||||
spacing=0.5,
|
||||
margin=0.5
|
||||
)
|
||||
return Popup(
|
||||
anchor=self.anchor,
|
||||
content=Rectangle(
|
||||
content=Column(
|
||||
content,
|
||||
Button(content=Text(text="Abbrechen", justify="center", fill=self.session.theme.secondary_color), shape="rectangle", style="colored-text", on_press=self.close),
|
||||
proportions=[2.5, 1]
|
||||
),
|
||||
fill=self.session.theme.hud_color,
|
||||
min_width=34,
|
||||
min_height=8.3
|
||||
),
|
||||
is_open=self.popup_open
|
||||
)
|
||||
@@ -1,44 +0,0 @@
|
||||
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,
|
||||
)
|
||||
@@ -1,222 +0,0 @@
|
||||
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,6 +2,7 @@ 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
|
||||
@@ -21,10 +22,10 @@ class TicketBuyCard(Component):
|
||||
available_tickets: int = 0
|
||||
|
||||
@event.on_populate
|
||||
async def on_populate(self) -> None:
|
||||
async def async_init(self) -> None:
|
||||
self.available_tickets = await self.session[TicketingService].get_available_tickets_for_category(self.category)
|
||||
|
||||
def build(self) -> Component:
|
||||
def build(self) -> rio.Component:
|
||||
ticket_description_style = TextStyle(
|
||||
fill=self.session.theme.neutral_color,
|
||||
font_size=1.2,
|
||||
|
||||
@@ -6,16 +6,14 @@ from rio import Component, Row, Text, TextStyle, Color
|
||||
class TournamentDetailsInfoRow(Component):
|
||||
key: str
|
||||
value: str
|
||||
key_color: Optional[Color] = None
|
||||
value_color: Optional[Color] = None
|
||||
|
||||
color: Optional[Color] = None
|
||||
|
||||
def build(self) -> Component:
|
||||
return Row(
|
||||
Text(
|
||||
text=self.key,
|
||||
style=TextStyle(
|
||||
fill=self.key_color if self.key_color is not None else self.session.theme.background_color,
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1
|
||||
),
|
||||
margin_bottom=0.5,
|
||||
@@ -24,7 +22,7 @@ class TournamentDetailsInfoRow(Component):
|
||||
Text(
|
||||
text=self.value,
|
||||
style=TextStyle(
|
||||
fill=self.value_color if self.value_color is not None else self.session.theme.background_color,
|
||||
fill=self.color if self.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,13 +35,8 @@ 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:
|
||||
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)
|
||||
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)
|
||||
else:
|
||||
self.profile_picture = await self.session[UserService].get_profile_picture(self.user.user_id)
|
||||
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
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.UserSession import UserSession
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
class UserInfoAndLoginBox(Component):
|
||||
state_changed_cb: Callable
|
||||
def build(self) -> Component:
|
||||
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)
|
||||
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)
|
||||
|
||||
@@ -6,7 +6,6 @@ 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
|
||||
@@ -14,7 +13,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.UserSession import UserSession
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
|
||||
|
||||
class StatusButton(Component):
|
||||
@@ -42,7 +41,6 @@ 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
|
||||
@@ -55,29 +53,31 @@ class UserInfoBox(Component):
|
||||
return choice(["Guten Tacho", "Tuten Gag", "Servus", "Moinjour", "Hallöchen", "Heyho", "Moinsen"])
|
||||
|
||||
async def logout(self) -> None:
|
||||
self.session.detach(UserSession)
|
||||
await self.session[SessionStorage].clear()
|
||||
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])
|
||||
if self.status_change_cb is not None:
|
||||
await self.status_change_cb()
|
||||
await self.session[RefreshService].trigger_refresh()
|
||||
self.session.navigate_to("")
|
||||
self.status_change_cb()
|
||||
self.session.navigate_to("/")
|
||||
|
||||
@event.on_populate
|
||||
async def async_init(self) -> None:
|
||||
self.user = await self.session[UserService].get_user(self.user_id)
|
||||
if self.user is not 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)
|
||||
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)
|
||||
if not self.user:
|
||||
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||
if not self.user:
|
||||
return
|
||||
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)
|
||||
|
||||
def build(self) -> Component:
|
||||
if not self.user:
|
||||
@@ -117,5 +117,5 @@ class UserInfoBox(Component):
|
||||
min_width=12,
|
||||
align_x=0.5,
|
||||
margin_top=0.3,
|
||||
margin_bottom=1.5
|
||||
margin_bottom=2
|
||||
)
|
||||
|
||||
@@ -3,32 +3,22 @@ from typing import Optional
|
||||
from rio import URL, GuardEvent
|
||||
|
||||
from src.ezgg_lan_manager.services.UserService import UserService
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
|
||||
|
||||
# Guards pages against access from users that are NOT logged in
|
||||
def logged_in_guard(event: GuardEvent) -> Optional[URL]:
|
||||
try:
|
||||
_ = event.session[UserSession].user_id
|
||||
return None
|
||||
except KeyError:
|
||||
if event.session[SessionStorage].user_id is None:
|
||||
return URL("./")
|
||||
|
||||
# Guards pages against access from users that ARE logged in
|
||||
def not_logged_in_guard(event: GuardEvent) -> Optional[URL]:
|
||||
try:
|
||||
_ = event.session[UserSession].user_id
|
||||
if event.session[SessionStorage].user_id is not None:
|
||||
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]:
|
||||
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:
|
||||
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:
|
||||
return URL("./")
|
||||
|
||||
@@ -1,39 +1,29 @@
|
||||
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, Image
|
||||
from rio import Column, Component, event, Text, TextStyle, Button, Color, Revealer, Row, ProgressCircle, Link
|
||||
|
||||
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")
|
||||
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}")
|
||||
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)
|
||||
|
||||
async def _on_banking_info_press(self) -> None:
|
||||
self.banking_info_revealer_open = not self.banking_info_revealer_open
|
||||
@@ -42,7 +32,7 @@ class AccountPage(Component):
|
||||
self.paypal_info_revealer_open = not self.paypal_info_revealer_open
|
||||
|
||||
def build(self) -> Component:
|
||||
if not self.user or not self.payment_qr_image:
|
||||
if not self.user and not self.balance:
|
||||
return Column(
|
||||
MainViewContentBox(
|
||||
ProgressCircle(
|
||||
@@ -91,10 +81,6 @@ 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,
|
||||
@@ -233,20 +219,19 @@ class AccountPage(Component):
|
||||
on_press=self._on_paypal_info_press
|
||||
),
|
||||
paypal_info_revealer,
|
||||
# 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
|
||||
# )
|
||||
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(
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
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, Link
|
||||
from rio import Component, event, Spacer, Card, Container, Column, Row, TextStyle, Color, Text, PageView, Button
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService, DatabaseService
|
||||
from src.ezgg_lan_manager.components.DesktopNavigation import DesktopNavigation
|
||||
@@ -58,15 +58,7 @@ class BasePage(Component):
|
||||
Row(
|
||||
Spacer(grow_x=True, grow_y=False),
|
||||
Card(
|
||||
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
|
||||
),
|
||||
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)),
|
||||
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, RefreshService
|
||||
from src.ezgg_lan_manager import ConfigurationService, UserService, TicketingService
|
||||
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,24 +19,14 @@ 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")
|
||||
try:
|
||||
user_id = self.session[UserSession].user_id
|
||||
except KeyError:
|
||||
self.user = None
|
||||
else:
|
||||
self.user = await self.session[UserService].get_user(user_id)
|
||||
self.user = await self.session[UserService].get_user(self.session[SessionStorage].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
|
||||
@@ -77,29 +67,17 @@ class BuyTicketPage(Component):
|
||||
|
||||
def build(self) -> Component:
|
||||
ticket_infos = self.session[ConfigurationService].get_ticket_info()
|
||||
header = Column(
|
||||
Text(
|
||||
"Tickets & Preise",
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
margin_top=2,
|
||||
align_x=0.5
|
||||
header = Text(
|
||||
"Tickets & Preise",
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
spacing=0.2
|
||||
margin_top=2,
|
||||
margin_bottom=2,
|
||||
align_x=0.5
|
||||
)
|
||||
|
||||
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
|
||||
from typing import Optional, Callable
|
||||
|
||||
from rio import Column, Component, event, TextStyle, Text, Spacer, Revealer, SwitcherBar, SwitcherBarChangeEvent, ProgressCircle
|
||||
|
||||
@@ -6,9 +6,8 @@ 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.UserSession import UserSession
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
|
||||
|
||||
class CateringPage(Component):
|
||||
@@ -16,6 +15,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:
|
||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Catering")
|
||||
@@ -32,10 +34,7 @@ class CateringPage(Component):
|
||||
return list(filter(lambda item: item.category == category, all_menu_items))
|
||||
|
||||
def build(self) -> Component:
|
||||
try:
|
||||
user_id = self.session[UserSession].user_id
|
||||
except KeyError:
|
||||
user_id = None
|
||||
user_id = self.session[SessionStorage].user_id
|
||||
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")
|
||||
try:
|
||||
self.user = await self.session[UserService].get_user(self.session[UserSession].user_id)
|
||||
except KeyError:
|
||||
if self.session[SessionStorage].user_id is not None:
|
||||
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||
else:
|
||||
self.user = None
|
||||
self.e_mail = "" if not self.user else self.user.user_mail
|
||||
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
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,14 +1,23 @@
|
||||
from typing import Optional
|
||||
|
||||
from rio import Column, Component, event, Spacer
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService
|
||||
from src.ezgg_lan_manager import ConfigurationService, UserService
|
||||
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_fallback_password = sha256(new_password.encode(encoding="utf-8")).hexdigest()
|
||||
user.user_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}",
|
||||
|
||||
@@ -3,7 +3,7 @@ from dataclasses import field, dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional, Callable
|
||||
|
||||
from rio import Column, Component, event, TextStyle, Text, Spacer, PointerEvent, Button, Popup, Card, Row, Rectangle, Color, PointerEventListener
|
||||
from rio import Column, Component, event, TextStyle, Text, Spacer, PointerEvent, Button, Popup, Card, Row
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService, CateringService, SeatingService, AccountingService
|
||||
from src.ezgg_lan_manager.components.CateringManagementOrderDisplay import CateringManagementOrderDisplay
|
||||
@@ -106,7 +106,6 @@ class ManageCateringPage(Component):
|
||||
return sorted_list
|
||||
|
||||
async def order_clicked(self, order: CateringOrder, _: PointerEvent) -> None:
|
||||
await self.update_orders()
|
||||
self.order_popup_order = order
|
||||
self.order_popup_open = True
|
||||
|
||||
@@ -121,7 +120,7 @@ class ManageCateringPage(Component):
|
||||
font_size=1.2
|
||||
),
|
||||
margin_top=2,
|
||||
margin_bottom=1,
|
||||
margin_bottom=2,
|
||||
align_x=0.5
|
||||
)
|
||||
popup = Popup(
|
||||
@@ -133,26 +132,7 @@ class ManageCateringPage(Component):
|
||||
)
|
||||
return Column(
|
||||
MainViewContentBox(
|
||||
Column(
|
||||
popup,
|
||||
PointerEventListener(
|
||||
content=Rectangle(
|
||||
content=Text(text="Neue Bestellung anlegen", fill=Color.WHITE, justify="center", margin=0.3),
|
||||
margin_bottom=1,
|
||||
margin_right=5,
|
||||
margin_left=5,
|
||||
fill=self.session.theme.secondary_color,
|
||||
hover_fill=self.session.theme.hud_color,
|
||||
stroke_width=0.2,
|
||||
stroke_color=Color.TRANSPARENT,
|
||||
hover_stroke_width=0.2,
|
||||
hover_stroke_color=self.session.theme.background_color,
|
||||
cursor="pointer",
|
||||
transition_time=0.1
|
||||
),
|
||||
on_press=lambda _: self.session.navigate_to("new-pos-order")
|
||||
)
|
||||
)
|
||||
Column(popup)
|
||||
),
|
||||
MainViewContentBox(
|
||||
Column(
|
||||
|
||||
@@ -1,132 +1,32 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from typing import Optional
|
||||
|
||||
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 rio import Column, Component, event, TextStyle, Text, Spacer
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService, TournamentService, UserService
|
||||
from src.ezgg_lan_manager import ConfigurationService
|
||||
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, TournamentError
|
||||
|
||||
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}")
|
||||
try:
|
||||
await self.session[TournamentService].start_tournament(tournament_id)
|
||||
except TournamentError as e:
|
||||
logger.error(f"Error trying to start tournament: {e}")
|
||||
|
||||
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:
|
||||
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=1,
|
||||
align_x=0.5
|
||||
MainViewContentBox(
|
||||
Column(
|
||||
Text(
|
||||
text="Turnier Verwaltung",
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
Button(
|
||||
content="Cache erneuern",
|
||||
shape="rectangle",
|
||||
style="colored-text",
|
||||
margin_bottom=2,
|
||||
align_x=0.5,
|
||||
on_press=self.session[TournamentService].queue_cache_renewal
|
||||
),
|
||||
*tournament_rows
|
||||
margin_top=2,
|
||||
margin_bottom=2,
|
||||
align_x=0.5
|
||||
)
|
||||
),
|
||||
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,17 +3,17 @@ from dataclasses import field
|
||||
from typing import Optional
|
||||
|
||||
from rio import Column, Component, event, TextStyle, Text, TextInput, ThemeContextSwitcher, Grid, \
|
||||
PointerEventListener, PointerEvent, Rectangle, Color, TextInputChangeEvent, Spacer, Row, Switch, \
|
||||
SwitchChangeEvent, EventHandler, Icon
|
||||
PointerEventListener, PointerEvent, Rectangle, CursorStyle, Color, TextInputChangeEvent, Spacer, Row, Switch, \
|
||||
SwitchChangeEvent, EventHandler
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService, UserService, AccountingService, SeatingService, MailingService
|
||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||
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="pointer"
|
||||
cursor=CursorStyle.POINTER
|
||||
),
|
||||
on_pointer_enter=self.on_mouse_enter,
|
||||
on_pointer_leave=self.on_mouse_leave,
|
||||
@@ -73,28 +73,18 @@ class ManageUsersPage(Component):
|
||||
seat = await self.session[SeatingService].get_user_seat(self.selected_user.user_id)
|
||||
self.user_seat = seat.seat_id if seat else "-"
|
||||
self.is_user_account_locked = not self.selected_user.is_active
|
||||
await self.on_search_parameters_changed(TextInputChangeEvent(self.selected_user.user_name))
|
||||
|
||||
async def on_search_parameters_changed(self, e: TextInputChangeEvent) -> None:
|
||||
self.search_results = list(
|
||||
filter(lambda user: (e.text.lower() in user.user_name.lower()) or e.text.lower() in str(user.user_id),
|
||||
self.all_users))
|
||||
|
||||
async def reset_view(self, _: PointerEvent) -> None:
|
||||
self.selected_user = None
|
||||
self.search_results = self.all_users
|
||||
await self.on_search_parameters_changed(TextInputChangeEvent(""))
|
||||
|
||||
async def change_account_active(self, _: SwitchChangeEvent) -> None:
|
||||
self.selected_user.is_active = not self.is_user_account_locked
|
||||
await self.session[UserService].update_user(self.selected_user)
|
||||
|
||||
async def on_new_transaction(self, transaction: Transaction) -> None:
|
||||
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:
|
||||
if not self.session[SessionStorage].is_team_member: # Better safe than sorry
|
||||
return
|
||||
|
||||
logger.info(f"Got new transaction for user with ID '{transaction.user_id}' over "
|
||||
@@ -104,7 +94,7 @@ class ManageUsersPage(Component):
|
||||
|
||||
if transaction.is_debit:
|
||||
try:
|
||||
new_total_balance = await self.session[AccountingService].remove_balance(
|
||||
await self.session[AccountingService].remove_balance(
|
||||
transaction.user_id,
|
||||
transaction.value,
|
||||
transaction.reference
|
||||
@@ -128,7 +118,6 @@ class ManageUsersPage(Component):
|
||||
|
||||
self.accounting_section_result_text = f"Guthaben {'entfernt' if transaction.is_debit else 'hinzugefügt'}!"
|
||||
self.accounting_section_result_success = True
|
||||
self.user_account_balance = self.session[AccountingService].make_euro_string_from_decimal(new_total_balance)
|
||||
|
||||
def build(self) -> Component:
|
||||
return Column(
|
||||
@@ -171,32 +160,15 @@ class ManageUsersPage(Component):
|
||||
),
|
||||
MainViewContentBox(
|
||||
Column(
|
||||
Row(
|
||||
Spacer(),
|
||||
PointerEventListener(
|
||||
content=Icon("material/cancel", fill="background", min_width=2.5, margin_top=1, margin_right=1),
|
||||
on_press=self.reset_view
|
||||
)
|
||||
),
|
||||
Row(
|
||||
Text(
|
||||
text=f"Konto von ",
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
justify="right"
|
||||
Text(
|
||||
text="Konto & Sitzplatz",
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
Text(
|
||||
text=self.selected_user.user_name,
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.hud_color,
|
||||
font_size=1.2
|
||||
),
|
||||
justify="left"
|
||||
),
|
||||
margin_top=0.5,
|
||||
margin_bottom=2
|
||||
margin_top=2,
|
||||
margin_bottom=2,
|
||||
align_x=0.5
|
||||
),
|
||||
Row(
|
||||
Text(
|
||||
|
||||
@@ -1,417 +0,0 @@
|
||||
import logging
|
||||
from asyncio import sleep, create_task
|
||||
from decimal import Decimal
|
||||
from typing import Optional, Callable
|
||||
|
||||
from rio import Column, Component, event, TextStyle, Text, Spacer, Revealer, ProgressCircle, ScrollContainer, Row, Popup, List, Rectangle, PointerEventListener, \
|
||||
PointerEvent, TextInput, TextInputChangeEvent
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService, CateringService, AccountingService
|
||||
from src.ezgg_lan_manager.components.CateringCartItem import CateringCartItem
|
||||
from src.ezgg_lan_manager.components.CateringSelectionItem import CateringSelectionItem
|
||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||
from src.ezgg_lan_manager.services.CateringService import CateringError, CateringErrorType
|
||||
from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItemCategory, CateringMenuItem
|
||||
from src.ezgg_lan_manager.types.CateringOrder import CateringMenuItemsWithAmount
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
|
||||
POPUP_CLOSE_TIMEOUT_SECONDS = 3
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
class Cart(Component):
|
||||
cart: List[CateringMenuItem]
|
||||
user_id: Optional[int]
|
||||
clear_cb: Callable
|
||||
order_button_loading: bool = False
|
||||
popup_message: str = ""
|
||||
popup_is_shown: bool = False
|
||||
popup_is_error: bool = True
|
||||
|
||||
async def on_remove_item(self, list_id: int) -> None:
|
||||
try:
|
||||
self.cart.pop(list_id)
|
||||
except IndexError:
|
||||
return
|
||||
|
||||
async def on_empty_cart_pressed(self, _: PointerEvent) -> None:
|
||||
self.cart.clear()
|
||||
|
||||
async def show_popup(self, text: str, is_error: bool) -> None:
|
||||
self.popup_is_error = is_error
|
||||
self.popup_message = text
|
||||
self.popup_is_shown = True
|
||||
self.force_refresh()
|
||||
await sleep(POPUP_CLOSE_TIMEOUT_SECONDS)
|
||||
self.popup_is_shown = False
|
||||
self.force_refresh()
|
||||
|
||||
async def on_order_pressed(self, _: PointerEvent) -> None:
|
||||
if self.user_id is None:
|
||||
return
|
||||
self.order_button_loading = True
|
||||
self.force_refresh()
|
||||
|
||||
show_popup_task = None
|
||||
if len(self.cart) < 1:
|
||||
show_popup_task = create_task(self.show_popup("Warenkorb leer", True))
|
||||
else:
|
||||
items_with_amounts: CateringMenuItemsWithAmount = {}
|
||||
for item in self.cart:
|
||||
try:
|
||||
items_with_amounts[item] += 1
|
||||
except KeyError:
|
||||
items_with_amounts[item] = 1
|
||||
try:
|
||||
await self.session[CateringService].place_order(items_with_amounts, self.user_id, is_delivery=False)
|
||||
except CateringError as catering_error:
|
||||
logger.error(catering_error)
|
||||
if catering_error.error_type == CateringErrorType.INCLUDES_DISABLED_ITEM:
|
||||
show_popup_task = create_task(self.show_popup("Warenkorb enthält gesperrte Artikel", True))
|
||||
elif catering_error.error_type == CateringErrorType.INSUFFICIENT_FUNDS:
|
||||
show_popup_task = create_task(self.show_popup("Guthaben nicht ausreichend", True))
|
||||
else:
|
||||
show_popup_task = create_task(self.show_popup(f"Unbekannter Fehler: {catering_error}", True))
|
||||
else:
|
||||
self.cart.clear()
|
||||
self.user_id = None
|
||||
await self.clear_cb()
|
||||
self.order_button_loading = False
|
||||
if not show_popup_task:
|
||||
show_popup_task = create_task(self.show_popup("Bestellung erfolgreich aufgegeben!", False))
|
||||
|
||||
def build(self) -> Component:
|
||||
cart_container = ScrollContainer(
|
||||
content=Column(
|
||||
*[CateringCartItem(
|
||||
article_name=cart_item.name,
|
||||
article_price=cart_item.price,
|
||||
article_id=cart_item.item_id,
|
||||
remove_item_cb=self.on_remove_item,
|
||||
list_id=idx
|
||||
) for idx, cart_item in enumerate(self.cart)],
|
||||
Spacer(grow_y=True)
|
||||
),
|
||||
min_height=8,
|
||||
min_width=33,
|
||||
margin=1
|
||||
)
|
||||
return Column(
|
||||
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),
|
||||
is_open=self.popup_is_shown,
|
||||
position="center",
|
||||
color=self.session.theme.primary_color
|
||||
),
|
||||
Row(
|
||||
Text(
|
||||
text=f"Preis: {AccountingService.make_euro_string_from_decimal(sum((cart_item.price for cart_item in self.cart), Decimal(0)))}",
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=0.8
|
||||
),
|
||||
margin=1
|
||||
),
|
||||
PointerEventListener(
|
||||
content=Rectangle(
|
||||
content=Text(
|
||||
"Warenkorb leeren",
|
||||
style=TextStyle(fill=self.session.theme.danger_color, font_size=0.9),
|
||||
justify="center",
|
||||
margin=0.2
|
||||
),
|
||||
hover_fill=self.session.theme.hud_color,
|
||||
transition_time=0.1,
|
||||
margin=0.5,
|
||||
cursor="pointer"
|
||||
),
|
||||
on_press=self.on_empty_cart_pressed
|
||||
),
|
||||
PointerEventListener(
|
||||
content=Rectangle(
|
||||
content=Text(
|
||||
"Bestellen",
|
||||
style=TextStyle(fill=self.session.theme.success_color, font_size=0.9),
|
||||
justify="center",
|
||||
margin=0.2
|
||||
),
|
||||
hover_fill=self.session.theme.hud_color if self.user_id is not None else self.session.theme.danger_color,
|
||||
transition_time=0.1,
|
||||
margin=0.5,
|
||||
cursor="pointer" if self.user_id is not None else "not-allowed"
|
||||
),
|
||||
on_press=self.on_order_pressed
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class NewPosOrderPage(Component):
|
||||
user_id_input_value: str = ""
|
||||
user_id: Optional[int] = None
|
||||
all_menu_items: Optional[list[CateringMenuItem]] = None
|
||||
cart: List[CateringMenuItem] = List()
|
||||
|
||||
|
||||
@event.on_populate
|
||||
async def on_populate(self) -> None:
|
||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Neue Bestellung anlegen")
|
||||
self.all_menu_items = await self.session[CateringService].get_menu()
|
||||
|
||||
async def on_user_logged_in_status_changed(self) -> None:
|
||||
self.force_refresh()
|
||||
|
||||
async def on_user_id_input_change(self, change_event: TextInputChangeEvent) -> None:
|
||||
try:
|
||||
id_ = int(change_event.text)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
self.user_id = id_
|
||||
|
||||
async def on_add(self, article_id: int) -> None:
|
||||
try:
|
||||
menu_item = await self.session[CateringService].get_menu_item_by_id(article_id)
|
||||
except CateringError as e:
|
||||
logger.error(e)
|
||||
return
|
||||
self.cart.append(menu_item)
|
||||
|
||||
@staticmethod
|
||||
def get_menu_items_by_category(all_menu_items: list[CateringMenuItem], category: Optional[CateringMenuItemCategory]) -> list[CateringMenuItem]:
|
||||
return list(filter(lambda item: item.category == category, all_menu_items))
|
||||
|
||||
async def clear_user_id_input(self) -> None:
|
||||
self.user_id_input_value = ""
|
||||
|
||||
def build(self) -> Component:
|
||||
try:
|
||||
is_team_member = self.session[UserSession].is_team_member
|
||||
except KeyError:
|
||||
is_team_member = False
|
||||
|
||||
|
||||
shopping_cart_container = MainViewContentBox(
|
||||
Column(
|
||||
Text(
|
||||
text="Neue Bestellung anlegen",
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
margin_top=2,
|
||||
margin_bottom=0.5,
|
||||
align_x=0.5
|
||||
),
|
||||
TextInput(text=self.bind().user_id_input_value, label="Nutzer ID", on_change=self.on_user_id_input_change, change_delay=1, margin_bottom=0.5, margin_left=5, margin_right=5),
|
||||
Cart(cart=self.cart, user_id=self.user_id, clear_cb=self.clear_user_id_input)
|
||||
)
|
||||
) if is_team_member else Spacer()
|
||||
|
||||
menu = [MainViewContentBox(
|
||||
ProgressCircle(
|
||||
color="secondary",
|
||||
align_x=0.5,
|
||||
margin_top=2,
|
||||
margin_bottom=2
|
||||
)
|
||||
)] if not self.all_menu_items else [MainViewContentBox(
|
||||
Revealer(
|
||||
header="Snacks",
|
||||
content=Column(
|
||||
*[CateringSelectionItem(
|
||||
article_name=catering_menu_item.name,
|
||||
article_price=catering_menu_item.price,
|
||||
article_id=catering_menu_item.item_id,
|
||||
on_add_callback=self.on_add,
|
||||
is_sensitive=not catering_menu_item.is_disabled,
|
||||
additional_info=catering_menu_item.additional_info,
|
||||
is_grey=idx % 2 == 0
|
||||
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.SNACK))],
|
||||
),
|
||||
header_style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
margin=1,
|
||||
align_y=0.5
|
||||
)
|
||||
),
|
||||
MainViewContentBox(
|
||||
Revealer(
|
||||
header="Frühstück",
|
||||
content=Column(
|
||||
*[CateringSelectionItem(
|
||||
article_name=catering_menu_item.name,
|
||||
article_price=catering_menu_item.price,
|
||||
article_id=catering_menu_item.item_id,
|
||||
on_add_callback=self.on_add,
|
||||
is_sensitive=not catering_menu_item.is_disabled,
|
||||
additional_info=catering_menu_item.additional_info,
|
||||
is_grey=idx % 2 == 0
|
||||
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.BREAKFAST))],
|
||||
),
|
||||
header_style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
margin=1,
|
||||
align_y=0.5
|
||||
)
|
||||
),
|
||||
MainViewContentBox(
|
||||
Revealer(
|
||||
header="Hauptspeisen",
|
||||
content=Column(
|
||||
*[CateringSelectionItem(
|
||||
article_name=catering_menu_item.name,
|
||||
article_price=catering_menu_item.price,
|
||||
article_id=catering_menu_item.item_id,
|
||||
on_add_callback=self.on_add,
|
||||
is_sensitive=not catering_menu_item.is_disabled,
|
||||
additional_info=catering_menu_item.additional_info,
|
||||
is_grey=idx % 2 == 0
|
||||
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.MAIN_COURSE))],
|
||||
),
|
||||
header_style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
margin=1,
|
||||
align_y=0.5
|
||||
)
|
||||
),
|
||||
MainViewContentBox(
|
||||
Revealer(
|
||||
header="Desserts",
|
||||
content=Column(
|
||||
*[CateringSelectionItem(
|
||||
article_name=catering_menu_item.name,
|
||||
article_price=catering_menu_item.price,
|
||||
article_id=catering_menu_item.item_id,
|
||||
on_add_callback=self.on_add,
|
||||
is_sensitive=not catering_menu_item.is_disabled,
|
||||
additional_info=catering_menu_item.additional_info,
|
||||
is_grey=idx % 2 == 0
|
||||
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.DESSERT))],
|
||||
),
|
||||
header_style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
margin=1,
|
||||
align_y=0.5
|
||||
)
|
||||
),
|
||||
MainViewContentBox(
|
||||
Revealer(
|
||||
header="Wasser & Softdrinks",
|
||||
content=Column(
|
||||
*[CateringSelectionItem(
|
||||
article_name=catering_menu_item.name,
|
||||
article_price=catering_menu_item.price,
|
||||
article_id=catering_menu_item.item_id,
|
||||
on_add_callback=self.on_add,
|
||||
is_sensitive=not catering_menu_item.is_disabled,
|
||||
additional_info=catering_menu_item.additional_info,
|
||||
is_grey=idx % 2 == 0
|
||||
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC))],
|
||||
),
|
||||
header_style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
margin=1,
|
||||
align_y=0.5
|
||||
)
|
||||
),
|
||||
MainViewContentBox(
|
||||
Revealer(
|
||||
header="Alkoholische Getränke",
|
||||
content=Column(
|
||||
*[CateringSelectionItem(
|
||||
article_name=catering_menu_item.name,
|
||||
article_price=catering_menu_item.price,
|
||||
article_id=catering_menu_item.item_id,
|
||||
on_add_callback=self.on_add,
|
||||
is_sensitive=not catering_menu_item.is_disabled,
|
||||
additional_info=catering_menu_item.additional_info,
|
||||
is_grey=idx % 2 == 0
|
||||
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC))],
|
||||
),
|
||||
header_style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
margin=1,
|
||||
align_y=0.5
|
||||
)
|
||||
),
|
||||
MainViewContentBox(
|
||||
Revealer(
|
||||
header="Cocktails & Longdrinks",
|
||||
content=Column(
|
||||
*[CateringSelectionItem(
|
||||
article_name=catering_menu_item.name,
|
||||
article_price=catering_menu_item.price,
|
||||
article_id=catering_menu_item.item_id,
|
||||
on_add_callback=self.on_add,
|
||||
is_sensitive=not catering_menu_item.is_disabled,
|
||||
additional_info=catering_menu_item.additional_info,
|
||||
is_grey=idx % 2 == 0
|
||||
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.BEVERAGE_COCKTAIL))],
|
||||
),
|
||||
header_style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
margin=1,
|
||||
align_y=0.5
|
||||
)
|
||||
),
|
||||
MainViewContentBox(
|
||||
Revealer(
|
||||
header="Shots",
|
||||
content=Column(
|
||||
*[CateringSelectionItem(
|
||||
article_name=catering_menu_item.name,
|
||||
article_price=catering_menu_item.price,
|
||||
article_id=catering_menu_item.item_id,
|
||||
on_add_callback=self.on_add,
|
||||
is_sensitive=not catering_menu_item.is_disabled,
|
||||
additional_info=catering_menu_item.additional_info,
|
||||
is_grey=idx % 2 == 0
|
||||
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.BEVERAGE_SHOT))],
|
||||
),
|
||||
header_style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
margin=1,
|
||||
align_y=0.5
|
||||
)
|
||||
),
|
||||
MainViewContentBox(
|
||||
Revealer(
|
||||
header="Sonstiges",
|
||||
content=Column(
|
||||
*[CateringSelectionItem(
|
||||
article_name=catering_menu_item.name,
|
||||
article_price=catering_menu_item.price,
|
||||
article_id=catering_menu_item.item_id,
|
||||
on_add_callback=self.on_add,
|
||||
is_sensitive=not catering_menu_item.is_disabled,
|
||||
additional_info=catering_menu_item.additional_info,
|
||||
is_grey=idx % 2 == 0
|
||||
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.NON_FOOD))],
|
||||
),
|
||||
header_style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1.2
|
||||
),
|
||||
margin=1,
|
||||
align_y=0.5
|
||||
)
|
||||
)]
|
||||
|
||||
return Column(shopping_cart_container, *menu, align_y=0)
|
||||
@@ -75,7 +75,7 @@ class OverviewPage(Component):
|
||||
Row(
|
||||
Text("Internet", fill=self.session.theme.neutral_color, margin_left=1),
|
||||
Spacer(),
|
||||
Text(f"100/50 Mbit/s (down/up)", fill=self.session.theme.neutral_color, margin_right=1),
|
||||
Text(f"60/20 Mbit/s (down/up)", fill=self.session.theme.neutral_color, margin_right=1),
|
||||
margin_bottom=0.3
|
||||
),
|
||||
Row(
|
||||
@@ -125,5 +125,17 @@ 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,154 +13,125 @@ 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 == self.pw_2) or len(self.pw_1) < MINIMUM_PASSWORD_LENGTH:
|
||||
self.pw_1_valid = False
|
||||
self.pw_2_valid = False
|
||||
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
|
||||
return
|
||||
self.pw_1_valid = True
|
||||
self.pw_2_valid = True
|
||||
self.pw_1.is_valid = True
|
||||
self.pw_2.is_valid = True
|
||||
|
||||
def on_email_focus_loss(self, change_event: TextInputChangeEvent) -> None:
|
||||
try:
|
||||
validate_email(change_event.text, check_deliverability=False)
|
||||
self.email_valid = True
|
||||
self.email_input.is_valid = True
|
||||
except EmailNotValidError:
|
||||
self.email_valid = False
|
||||
self.email_input.is_valid = False
|
||||
|
||||
def on_user_name_focus_loss(self, _: TextInputChangeEvent) -> None:
|
||||
current_text = self.user_name
|
||||
current_text = self.user_name_input.text
|
||||
if len(current_text) > UserService.MAX_USERNAME_LENGTH:
|
||||
self.user_name = current_text[:UserService.MAX_USERNAME_LENGTH]
|
||||
self.user_name_input.text = current_text[:UserService.MAX_USERNAME_LENGTH]
|
||||
|
||||
async def on_submit_button_pressed(self) -> None:
|
||||
self.submit_button_loading = True
|
||||
self.submit_button.is_loading = True
|
||||
self.submit_button.force_refresh()
|
||||
|
||||
if len(self.user_name) < 1:
|
||||
await self.display_animated_text(False, "Nutzername darf nicht leer sein!")
|
||||
self.submit_button_loading = False
|
||||
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
|
||||
return
|
||||
|
||||
if not (self.pw_1 == self.pw_2):
|
||||
await self.display_animated_text(False, "Passwörter stimmen nicht überein!")
|
||||
self.submit_button_loading = False
|
||||
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
|
||||
return
|
||||
|
||||
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
|
||||
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
|
||||
return
|
||||
|
||||
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
|
||||
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
|
||||
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) 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
|
||||
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
|
||||
return
|
||||
|
||||
try:
|
||||
new_user = await user_service.create_user(self.user_name, self.email, self.pw_1)
|
||||
new_user = await user_service.create_user(self.user_name_input.text, self.email_input.text, self.pw_1.text)
|
||||
if not new_user:
|
||||
logger.error(f"create_user returned: {new_user}")
|
||||
raise Exception(f"create_user returned: {new_user}")
|
||||
logger.warning(f"UserService.create_user returned: {new_user}") # ToDo: Seems like the user is created fine, even if not returned #FixMe
|
||||
except Exception as e:
|
||||
logger.error(f"Unknown error during new user registration: {e}")
|
||||
await self.display_animated_text(False, "Es ist ein unbekannter Fehler aufgetreten :(")
|
||||
self.submit_button_loading = False
|
||||
await self.animated_text.display_text(False, "Es ist ein unbekannter Fehler aufgetreten :(")
|
||||
self.submit_button.is_loading = False
|
||||
return
|
||||
|
||||
await mailing_service.send_email(
|
||||
subject="Erfolgreiche Registrierung",
|
||||
body=f"Hallo {self.user_name},\n\n"
|
||||
body=f"Hallo {self.user_name_input.text},\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
|
||||
receiver=self.email_input.text
|
||||
)
|
||||
|
||||
self.submit_button_loading = False
|
||||
await self.display_animated_text(True, "Erfolgreich registriert!")
|
||||
self.submit_button.is_loading = False
|
||||
await self.animated_text.display_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:
|
||||
user_name_input = TextInput(
|
||||
self.user_name_input = TextInput(
|
||||
label="Benutzername",
|
||||
text=self.bind().user_name,
|
||||
text="",
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
margin_bottom=1,
|
||||
grow_x=True,
|
||||
on_lose_focus=self.on_user_name_focus_loss
|
||||
)
|
||||
email_input = TextInput(
|
||||
self.email_input = TextInput(
|
||||
label="E-Mail Adresse",
|
||||
text=self.bind().email,
|
||||
text="",
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
margin_bottom=1,
|
||||
grow_x=True,
|
||||
on_lose_focus=self.on_email_focus_loss,
|
||||
is_valid=self.email_valid
|
||||
on_lose_focus=self.on_email_focus_loss
|
||||
)
|
||||
pw_1_input = TextInput(
|
||||
self.pw_1 = TextInput(
|
||||
label="Passwort",
|
||||
text=self.bind().pw_1,
|
||||
text="",
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
margin_bottom=1,
|
||||
grow_x=True,
|
||||
is_secret=True,
|
||||
on_lose_focus=self.on_pw_focus_loss,
|
||||
is_valid=self.pw_1_valid
|
||||
on_lose_focus=self.on_pw_focus_loss
|
||||
)
|
||||
pw_2_input = TextInput(
|
||||
self.pw_2 = TextInput(
|
||||
label="Passwort wiederholen",
|
||||
text=self.bind().pw_2,
|
||||
text="",
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
margin_bottom=1,
|
||||
grow_x=True,
|
||||
is_secret=True,
|
||||
on_lose_focus=self.on_pw_focus_loss,
|
||||
is_valid=self.pw_2_valid
|
||||
on_lose_focus=self.on_pw_focus_loss
|
||||
)
|
||||
submit_button = Button(
|
||||
self.submit_button = Button(
|
||||
content=Text(
|
||||
"Registrieren",
|
||||
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9),
|
||||
@@ -174,8 +145,13 @@ class RegisterPage(Component):
|
||||
shape="rectangle",
|
||||
style="minor",
|
||||
color=self.session.theme.secondary_color,
|
||||
on_press=self.on_submit_button_pressed,
|
||||
is_loading=self.submit_button_loading
|
||||
on_press=self.on_submit_button_pressed
|
||||
)
|
||||
self.animated_text = AnimatedText(
|
||||
margin_top=2,
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
margin_bottom=2
|
||||
)
|
||||
return Column(
|
||||
MainViewContentBox(
|
||||
@@ -190,12 +166,12 @@ class RegisterPage(Component):
|
||||
margin_bottom=2,
|
||||
align_x=0.5
|
||||
),
|
||||
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)
|
||||
self.user_name_input,
|
||||
self.email_input,
|
||||
self.pw_1,
|
||||
self.pw_2,
|
||||
self.submit_button,
|
||||
self.animated_text
|
||||
)
|
||||
),
|
||||
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,10 +37,7 @@ 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()
|
||||
try:
|
||||
self.user = await self.session[UserService].get_user(self.session[UserSession].user_id)
|
||||
except KeyError:
|
||||
self.user = None
|
||||
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||
if not self.user:
|
||||
self.is_booking_blocked = True
|
||||
else:
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
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,37 +1,23 @@
|
||||
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, Revealer, PointerEventListener, \
|
||||
PointerEvent, Rectangle, Color, Popup, Dropdown
|
||||
from rio import Column, Component, event, TextStyle, Text, Row, Image, Spacer, ProgressCircle, Button, Checkbox, ThemeContextSwitcher, Link
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService, TournamentService, UserService, TicketingService, TeamService, RefreshService
|
||||
from src.ezgg_lan_manager import ConfigurationService, TournamentService, UserService
|
||||
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.Team import Team, TeamStatus
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
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, ParticipantType, TournamentFormat
|
||||
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.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
|
||||
@@ -44,143 +30,38 @@ class TournamentDetailsPage(Component):
|
||||
tournament_id = None
|
||||
if tournament_id is not None:
|
||||
self.tournament = await self.session[TournamentService].get_tournament_by_id(tournament_id)
|
||||
if isinstance(self.tournament, Tournament):
|
||||
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")
|
||||
|
||||
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.session[RefreshService].subscribe(self.on_populate)
|
||||
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||
|
||||
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:
|
||||
if isinstance(self.tournament, Tournament):
|
||||
self.tournament = await self.session[TournamentService].get_tournament_by_id(self.tournament.id)
|
||||
if self.tournament is None or isinstance(self.tournament, str):
|
||||
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
|
||||
|
||||
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"
|
||||
elif not isinstance(self.tournament, Tournament):
|
||||
self.is_success = False
|
||||
self.message = "Fehler bei der Anmeldung"
|
||||
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.is_success = True
|
||||
self.message = f"Erfolgreich angemeldet!" # ToDo: Hook into Tournament Service
|
||||
self.loading = False
|
||||
|
||||
async def on_team_register_confirmed(self) -> None:
|
||||
if self.team_selected_for_register is None:
|
||||
await self.on_team_register_canceled()
|
||||
return
|
||||
try:
|
||||
if isinstance(self.tournament, Tournament):
|
||||
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
|
||||
else:
|
||||
raise ValueError("Turnier nicht gefunden")
|
||||
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:
|
||||
async def unregister_pressed(self) -> None:
|
||||
self.loading = True
|
||||
if not self.user:
|
||||
return
|
||||
|
||||
try:
|
||||
if isinstance(self.tournament, Tournament) and self.tournament.participant_type == ParticipantType.PLAYER:
|
||||
await self.session[TournamentService].unregister_user_from_tournament(self.user.user_id, self.tournament.id)
|
||||
elif isinstance(self.tournament, Tournament) and 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.is_success = True
|
||||
self.message = f"Erfolgreich abgemeldet!" # ToDo: Hook into Tournament Service
|
||||
self.loading = False
|
||||
|
||||
async def tree_button_clicked(self) -> None:
|
||||
if isinstance(self.tournament, Tournament):
|
||||
self.session.navigate_to(f"tournament-tree?id={self.tournament.id}")
|
||||
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:
|
||||
@@ -215,59 +96,33 @@ class TournamentDetailsPage(Component):
|
||||
tournament_status_color = self.session.theme.danger_color
|
||||
elif self.tournament.status == TournamentStatus.ONGOING or self.tournament.status == TournamentStatus.COMPLETED:
|
||||
tournament_status_color = self.session.theme.warning_color
|
||||
if self.tournament.format != TournamentFormat.FFA:
|
||||
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
|
||||
)
|
||||
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.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
|
||||
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:
|
||||
logger.fatal("Did someone add new values to ParticipantType ? ;)")
|
||||
return Column()
|
||||
|
||||
# This should NEVER happen
|
||||
button_text = "Anmelden"
|
||||
button_sensitive_hook = False
|
||||
|
||||
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
|
||||
@@ -293,9 +148,11 @@ 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}"), margin_right=1),
|
||||
Image(image=from_root(f"src/ezgg_lan_manager/assets/img/games/{self.tournament.game_title.image_name}")),
|
||||
Text(
|
||||
text=self.tournament.name,
|
||||
style=TextStyle(
|
||||
@@ -310,32 +167,14 @@ class TournamentDetailsPage(Component):
|
||||
margin_left=6
|
||||
),
|
||||
Spacer(min_height=1),
|
||||
TournamentDetailsInfoRow("Status", tournament_status_to_display_text(self.tournament.status), value_color=tournament_status_color),
|
||||
TournamentDetailsInfoRow("Status", tournament_status_to_display_text(self.tournament.status), 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]),
|
||||
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
|
||||
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
|
||||
),
|
||||
tree_button,
|
||||
Row(
|
||||
@@ -360,39 +199,6 @@ class TournamentDetailsPage(Component):
|
||||
button
|
||||
)
|
||||
|
||||
if isinstance(self.tournament, 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(
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional, Union
|
||||
|
||||
from from_root import from_root
|
||||
from rio import Column, Component, event, TextStyle, Text, Row, Spacer, ProgressCircle, Rectangle, Stack
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService, TournamentService, UserService, TeamService, RefreshService, SeatingService
|
||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||
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 ParticipantType, TournamentFormat
|
||||
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 MatchInfo(Component):
|
||||
opponent_1: str = ""
|
||||
opponent_2: str = ""
|
||||
opponent_1_seat: str = ""
|
||||
opponent_2_seat: str = ""
|
||||
winner: str = ""
|
||||
|
||||
def build(self) -> Component:
|
||||
return Rectangle(
|
||||
content=Column(
|
||||
Stack(
|
||||
Row(
|
||||
Row(
|
||||
Text(
|
||||
text=self.opponent_1,
|
||||
style=TextStyle(fill=self.session.theme.success_color if self.winner == self.opponent_1 else self.session.theme.background_color),
|
||||
justify="left",
|
||||
margin_right=0.6,
|
||||
font_size=0.9
|
||||
),
|
||||
Text(
|
||||
text=f"({self.opponent_1_seat})" if self.opponent_1_seat else "Freilos",
|
||||
style=TextStyle(fill=self.session.theme.background_color),
|
||||
justify="left",
|
||||
font_size=0.9
|
||||
)
|
||||
),
|
||||
Spacer(),
|
||||
Row(
|
||||
Text(
|
||||
text=self.opponent_2,
|
||||
style=TextStyle(fill=self.session.theme.success_color if self.winner == self.opponent_2 else self.session.theme.background_color),
|
||||
justify="right",
|
||||
margin_right=0.6,
|
||||
font_size=0.9
|
||||
),
|
||||
Text(
|
||||
text=f"({self.opponent_2_seat})" if self.opponent_2_seat else "Freilos",
|
||||
style=TextStyle(fill=self.session.theme.background_color),
|
||||
justify="right",
|
||||
font_size=0.9
|
||||
)
|
||||
),
|
||||
margin=0.3
|
||||
),
|
||||
Row(
|
||||
Text(
|
||||
text=f"vs.",
|
||||
style=TextStyle(fill=self.session.theme.background_color),
|
||||
justify="center"
|
||||
),
|
||||
margin=0.3
|
||||
)
|
||||
)
|
||||
),
|
||||
margin=1,
|
||||
stroke_width=0.2,
|
||||
stroke_color=self.session.theme.background_color,
|
||||
fill=self.session.theme.hud_color,
|
||||
)
|
||||
|
||||
class TournamentTreePage(Component):
|
||||
tournament: Optional[Union[Tournament, str]] = None
|
||||
user: Optional[User] = None
|
||||
teams: list[Team] = []
|
||||
id_to_username_map: dict[int, str] = {}
|
||||
id_to_seat_map: dict[int, str] = {}
|
||||
is_fully_loaded: bool = False
|
||||
|
||||
@event.on_populate
|
||||
async def on_populate(self) -> None:
|
||||
try:
|
||||
tournament_id = int(self.session.active_page_url.query_string.split("id=")[-1])
|
||||
except (ValueError, AttributeError, TypeError):
|
||||
tournament_id = None
|
||||
if tournament_id is not None:
|
||||
self.tournament = await self.session[TournamentService].get_tournament_by_id(tournament_id)
|
||||
if isinstance(self.tournament, Tournament):
|
||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - {self.tournament.name}")
|
||||
else:
|
||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Turniere")
|
||||
|
||||
try:
|
||||
user_id = self.session[UserSession].user_id
|
||||
self.user = await self.session[UserService].get_user(user_id)
|
||||
except KeyError:
|
||||
self.user = None
|
||||
|
||||
self.teams = await self.session[TeamService].get_all_teams()
|
||||
|
||||
all_users = await self.session[UserService].get_all_users()
|
||||
id_to_username_map = {}
|
||||
id_to_seat_map = {}
|
||||
for user in all_users:
|
||||
id_to_username_map[user.user_id] = user.user_name
|
||||
seat = await self.session[SeatingService].get_user_seat(user.user_id)
|
||||
if seat is not None:
|
||||
id_to_seat_map[user.user_id] = seat.seat_id
|
||||
|
||||
self.id_to_username_map = id_to_username_map
|
||||
self.id_to_seat_map = id_to_seat_map
|
||||
self.session[RefreshService].subscribe(self.on_populate)
|
||||
self.is_fully_loaded = True
|
||||
|
||||
def _get_seat_for_team(self, team: Team) -> str:
|
||||
# Retrieves seat id for leader of a team
|
||||
leader = list(team.members.keys())[0]
|
||||
for member, rank in team.members.items():
|
||||
if rank == TeamStatus.LEADER:
|
||||
leader = member
|
||||
break
|
||||
|
||||
return self.id_to_seat_map[leader.user_id]
|
||||
|
||||
|
||||
def build(self) -> Component:
|
||||
if self.tournament is None or not self.is_fully_loaded:
|
||||
return Column(
|
||||
MainViewContentBox(
|
||||
Column(
|
||||
Spacer(min_height=1),
|
||||
Column(
|
||||
ProgressCircle(
|
||||
color="secondary",
|
||||
align_x=0.5,
|
||||
margin_top=0,
|
||||
margin_bottom=0
|
||||
),
|
||||
min_height=10
|
||||
),
|
||||
Spacer(min_height=1)
|
||||
)
|
||||
),
|
||||
align_y=0
|
||||
)
|
||||
elif isinstance(self.tournament, str):
|
||||
content = Row(
|
||||
Text(
|
||||
text=self.tournament,
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=1
|
||||
),
|
||||
margin_top=2,
|
||||
margin_bottom=2,
|
||||
align_x=0.5
|
||||
)
|
||||
)
|
||||
else:
|
||||
if self.tournament.format == TournamentFormat.FFA:
|
||||
content = Column(
|
||||
Text(
|
||||
text=f"Dieses Turnier hat keinen Turnierbaum.",
|
||||
style=TextStyle(fill=self.session.theme.background_color),
|
||||
margin_top=1,
|
||||
margin_bottom=1,
|
||||
align_x=0.5,
|
||||
overflow="wrap",
|
||||
min_width=30,
|
||||
justify="center"
|
||||
)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
file_name = self.tournament.name.replace(" ", "_") + ".json"
|
||||
games_per_matchup = int(self.tournament.format.name[-1])
|
||||
logger.info(f"Trying to read tournament data from {file_name}")
|
||||
with open(from_root("tournament_data", file_name), "r") as f:
|
||||
json_data = json.load(f)
|
||||
|
||||
last_valid_round = None
|
||||
round_num = 0
|
||||
for round_ in json_data["rounds"]:
|
||||
if all(
|
||||
match["opponent_1_id"] is not None or match["opponent_2_id"] is not None
|
||||
for match in round_
|
||||
):
|
||||
last_valid_round = round_
|
||||
round_num += 1
|
||||
|
||||
if last_valid_round is None:
|
||||
last_valid_round = json_data["rounds"][0]
|
||||
|
||||
match_infos = []
|
||||
if self.tournament.participant_type == ParticipantType.PLAYER:
|
||||
match_infos = [MatchInfo(
|
||||
opponent_1=self.id_to_username_map.get(match["opponent_1_id"], ""),
|
||||
opponent_2=self.id_to_username_map.get(match["opponent_2_id"], ""),
|
||||
winner=self.id_to_username_map.get(match["winner"], ""),
|
||||
opponent_1_seat=self.id_to_seat_map.get(match["opponent_1_id"], ""),
|
||||
opponent_2_seat=self.id_to_seat_map.get(match["opponent_2_id"], ""),
|
||||
) for match in last_valid_round]
|
||||
elif self.tournament.participant_type == ParticipantType.TEAM:
|
||||
for match in last_valid_round:
|
||||
team_1: Optional[Team] = next(filter(lambda t: t.id == match["opponent_1_id"], self.teams), None)
|
||||
team_2: Optional[Team] = next(filter(lambda t: t.id == match["opponent_2_id"], self.teams), None)
|
||||
winner: Union[str, Team] = next(filter(lambda t: t.id == match["winner"], self.teams), "")
|
||||
match_infos.append(
|
||||
MatchInfo(
|
||||
opponent_1=team_1.name if team_1 is not None else "",
|
||||
opponent_2=team_2.name if team_2 is not None else "",
|
||||
winner=winner if isinstance(winner, str) else winner.name,
|
||||
opponent_1_seat=self._get_seat_for_team(team_1) if team_1 is not None else "",
|
||||
opponent_2_seat=self._get_seat_for_team(team_2) if team_2 is not None else "",
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise ValueError("Unknown participant type")
|
||||
|
||||
|
||||
|
||||
content = Column(
|
||||
Text(
|
||||
text=f"{self.tournament.name}",
|
||||
style=TextStyle(fill=self.session.theme.background_color),
|
||||
justify="center",
|
||||
font_size=1.2
|
||||
),
|
||||
Text(
|
||||
text="Finale" if len(json_data["rounds"]) == round_num else f"Runde {round_num}",
|
||||
style=TextStyle(fill=self.session.theme.background_color),
|
||||
justify="center",
|
||||
font_size=0.9,
|
||||
margin_bottom=1
|
||||
),
|
||||
Text(
|
||||
text=f"Spiele pro Matchup: {games_per_matchup}",
|
||||
style=TextStyle(fill=self.session.theme.background_color),
|
||||
justify="center",
|
||||
font_size=0.8
|
||||
),
|
||||
Text(
|
||||
text=f"Melde als Verlierer deinen Matchausgang\nim Discord oder an der Orga-Ecke",
|
||||
style=TextStyle(fill=self.session.theme.background_color),
|
||||
justify="center",
|
||||
font_size=0.8
|
||||
),
|
||||
*match_infos
|
||||
)
|
||||
except (FileNotFoundError, ValueError, AttributeError):
|
||||
content = Column(
|
||||
Text(
|
||||
text=f"Der Turnierbaum für dieses Turnier steht leider nicht zur Verfügung.\n\nBitte melde sich beim Orga-Team.",
|
||||
style=TextStyle(fill=self.session.theme.background_color),
|
||||
margin_top=1,
|
||||
margin_bottom=1,
|
||||
align_x=0.5,
|
||||
overflow="wrap",
|
||||
min_width=30,
|
||||
justify="center"
|
||||
)
|
||||
)
|
||||
|
||||
return Column(
|
||||
MainViewContentBox(
|
||||
Column(
|
||||
Spacer(min_height=1),
|
||||
content,
|
||||
Spacer(min_height=1)
|
||||
)
|
||||
),
|
||||
align_y=0
|
||||
)
|
||||
@@ -22,8 +22,3 @@ 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
|
||||
from .TournamentTreePage import TournamentTreePage
|
||||
from .NewPosOrderPage import NewPosOrderPage
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import io
|
||||
import logging
|
||||
import qrcode
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, ROUND_DOWN
|
||||
@@ -76,29 +74,3 @@ 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()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
|
||||
from datetime import date, datetime, UTC
|
||||
from datetime import date, datetime
|
||||
from pprint import pprint
|
||||
from typing import Optional
|
||||
from decimal import Decimal
|
||||
|
||||
@@ -13,10 +14,9 @@ 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, MatchStatus
|
||||
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
|
||||
|
||||
@@ -62,8 +62,7 @@ class DatabaseService:
|
||||
password=self._database_config.db_password,
|
||||
db=self._database_config.db_name,
|
||||
minsize=1,
|
||||
maxsize=40,
|
||||
autocommit=True
|
||||
maxsize=40
|
||||
)
|
||||
except aiomysql.OperationalError:
|
||||
return False
|
||||
@@ -76,15 +75,14 @@ class DatabaseService:
|
||||
user_name=data[1],
|
||||
user_mail=data[2],
|
||||
user_password=data[3],
|
||||
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]
|
||||
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]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -101,8 +99,6 @@ class DatabaseService:
|
||||
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}")
|
||||
@@ -187,10 +183,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_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,
|
||||
"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,
|
||||
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)
|
||||
@@ -449,7 +445,7 @@ class DatabaseService:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.delete_ticket(ticket_id)
|
||||
return await self.change_ticket_owner(ticket_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error deleting ticket: {e}")
|
||||
return False
|
||||
@@ -861,7 +857,6 @@ class DatabaseService:
|
||||
t.status AS tournament_status,
|
||||
t.max_participants,
|
||||
t.created_at,
|
||||
t.participant_type AS tournament_participant_type,
|
||||
|
||||
/* =======================
|
||||
Game Title
|
||||
@@ -877,7 +872,6 @@ class DatabaseService:
|
||||
======================= */
|
||||
tp.id AS participant_id,
|
||||
tp.user_id,
|
||||
tp.team_id,
|
||||
tp.participant_type,
|
||||
tp.seed,
|
||||
tp.joined_at
|
||||
@@ -911,8 +905,6 @@ class DatabaseService:
|
||||
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"],
|
||||
@@ -926,277 +918,16 @@ class DatabaseService:
|
||||
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 [],
|
||||
participants=[Participant(id_=row["user_id"], participant_type=self._parse_participant_type(row["participant_type"]))],
|
||||
matches=None, # ToDo: Implement
|
||||
rounds=[], # ToDo: Implement
|
||||
max_participants=row["max_participants"],
|
||||
participant_type=participant_type
|
||||
max_participants=row["max_participants"]
|
||||
)
|
||||
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"]))
|
||||
Participant(id_=row["user_id"], 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)
|
||||
|
||||
async def change_tournament_status(self, tournament_id: int, status: TournamentStatus) -> None:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"UPDATE tournaments SET status = %s WHERE (id = %s)",
|
||||
(status.name, tournament_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.change_tournament_status(tournament_id, status)
|
||||
|
||||
@@ -3,24 +3,23 @@ from typing import Optional
|
||||
|
||||
from rio import UserSettings
|
||||
|
||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
|
||||
|
||||
class LocalData(UserSettings):
|
||||
stored_session_token: Optional[str]
|
||||
stored_session_token: Optional[str] = None
|
||||
|
||||
class LocalDataService:
|
||||
def __init__(self) -> None:
|
||||
self._session: dict[str, UserSession] = {}
|
||||
self._session: dict[str, SessionStorage] = {}
|
||||
|
||||
def verify_token(self, token: str) -> Optional[UserSession]:
|
||||
def verify_token(self, token: str) -> Optional[SessionStorage]:
|
||||
return self._session.get(token)
|
||||
|
||||
def set_session(self, session: UserSession) -> str:
|
||||
def set_session(self, session: SessionStorage) -> str:
|
||||
key = secrets.token_hex(32)
|
||||
self._session[key] = session
|
||||
return key
|
||||
|
||||
def del_session(self, token: Optional[str]) -> None:
|
||||
if token is not None:
|
||||
self._session.pop(token, None)
|
||||
def del_session(self, token: str) -> None:
|
||||
self._session.pop(token, None)
|
||||
|
||||
@@ -45,7 +45,7 @@ class MailingService:
|
||||
return f"""
|
||||
Hallo {user.user_name},
|
||||
|
||||
deinem Account wurden {added_balance:.2f} € hinzugefügt. Dein neues Guthaben beträgt nun {total_balance:.2f} €.
|
||||
deinem Account wurden {added_balance} € hinzugefügt. Dein neues Guthaben beträgt nun {total_balance} €.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -15,15 +15,11 @@ class ReceiptPrintingService:
|
||||
self._seating_service = seating_service
|
||||
self._config = config
|
||||
self._dev_mode_enabled = dev_mode_enabled
|
||||
self._url = f"http://{self._config.host}:{self._config.port}/{self._config.order_print_endpoint}"
|
||||
|
||||
async def print_order(self, user: User, order: CateringOrder) -> None:
|
||||
seat = await self._seating_service.get_user_seat(user.user_id)
|
||||
if seat is None:
|
||||
seat_id = await self._seating_service.get_user_seat(user.user_id)
|
||||
if not seat_id:
|
||||
seat_id = " - "
|
||||
else:
|
||||
seat_id = str(seat.seat_id)
|
||||
|
||||
|
||||
menu_items_payload = []
|
||||
for item, amount in order.items.items():
|
||||
@@ -39,19 +35,14 @@ class ReceiptPrintingService:
|
||||
"seat_id": seat_id,
|
||||
"items": menu_items_payload
|
||||
}
|
||||
|
||||
logger.info(f"Sending print order to {self._url}: {payload}")
|
||||
try:
|
||||
response = requests.post(
|
||||
self._url,
|
||||
requests.post(
|
||||
f"http://{self._config.host}:{self._config.port}/{self._config.order_print_endpoint}",
|
||||
json=payload,
|
||||
headers={"x-password": self._config.password},
|
||||
timeout=2.0
|
||||
headers={"x-password": self._config.password}
|
||||
)
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Received an error with code {response.status_code}: {response.text}")
|
||||
except Exception as e:
|
||||
if self._dev_mode_enabled:
|
||||
logger.info("An error occurred trying to print a receipt: %s", e)
|
||||
logger.info("An error occurred trying to print a receipt:", e)
|
||||
return
|
||||
logger.error("An error occurred trying to print a receipt: %s", e)
|
||||
logger.error("An error occurred trying to print a receipt:", e)
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
from typing import Callable, Optional
|
||||
|
||||
|
||||
class RefreshService:
|
||||
"""
|
||||
The active rio.Components can subscribe to this service with their on_populate method.
|
||||
This methods get called whenever a overall refresh is needed. Usually when the user logs in or out.
|
||||
"""
|
||||
def __init__(self) -> None:
|
||||
self.subscriber: Optional[Callable] = None
|
||||
|
||||
def subscribe(self, refresh_cb: Callable) -> None:
|
||||
self.subscriber = refresh_cb
|
||||
|
||||
async def trigger_refresh(self) -> None:
|
||||
if self.subscriber is not None:
|
||||
await self.subscriber()
|
||||
@@ -1,134 +0,0 @@
|
||||
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,30 +1,69 @@
|
||||
import json
|
||||
from pprint import pprint
|
||||
from asyncio import sleep
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from from_root import from_root
|
||||
|
||||
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 ParticipantType, TournamentError
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
class TournamentService:
|
||||
def __init__(self, db_service: DatabaseService, user_service: UserService) -> None:
|
||||
self._db_service = db_service
|
||||
self._user_service = user_service
|
||||
|
||||
# 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 queue_cache_renewal(self) -> None:
|
||||
# Used in admin UI to provoke cache renewal after direct database access
|
||||
self._cache_dirty = True
|
||||
|
||||
# This overrides the database access and is meant for easy development.
|
||||
# Set to None before merging back into main.
|
||||
self._dev_data = [
|
||||
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, 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
|
||||
)
|
||||
]
|
||||
|
||||
async def _update_cache(self) -> None:
|
||||
tournaments = await self._db_service.get_all_tournaments()
|
||||
@@ -32,47 +71,6 @@ class TournamentService:
|
||||
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]:
|
||||
if self._cache_dirty:
|
||||
@@ -83,62 +81,3 @@ class TournamentService:
|
||||
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()
|
||||
await self._generate_initial_json_file(tournament)
|
||||
await self._db_service.change_tournament_status(tournament_id, tournament.status)
|
||||
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()
|
||||
await self._db_service.change_tournament_status(tournament_id, tournament.status)
|
||||
self._cache_dirty = True
|
||||
|
||||
async def _generate_initial_json_file(self, tournament: Tournament) -> None:
|
||||
"""
|
||||
Generates the initial JSON file for the tournament. Won't generate a new one if one already exists.
|
||||
ToDo: Remove this method when final tournament system is completed.
|
||||
"""
|
||||
p = tournament.participants
|
||||
pairs = [
|
||||
(p[i], p[i + 1]) if i + 1 < len(p) else (p[i], None)
|
||||
for i in range(0, len(p), 2)
|
||||
]
|
||||
data = {
|
||||
"rounds": [
|
||||
[
|
||||
{
|
||||
"opponent_1_id": pair[0].id if pair[0] is not None else None,
|
||||
"opponent_2_id": pair[1].id if pair[1] is not None else None,
|
||||
"winner": None
|
||||
} for pair in pairs
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
# Resolve byes
|
||||
for match in data["rounds"][0]:
|
||||
if match["opponent_2_id"] is None:
|
||||
match["winner"] = match["opponent_1_id"]
|
||||
|
||||
file_name = tournament.name.replace(" ", "_") + ".json"
|
||||
try:
|
||||
with open(from_root("tournament_data", file_name), "x") as f:
|
||||
json.dump(data, f, indent=4)
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
@@ -59,12 +59,9 @@ 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
|
||||
if user.user_fallback_password and user.user_fallback_password == user_password_hash:
|
||||
return True
|
||||
return user.user_password == user_password_hash
|
||||
return user.user_password == sha256(password_clear_text.encode(encoding="utf-8")).hexdigest()
|
||||
|
||||
|
||||
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, 1, 2]) -> None:
|
||||
def __init__(self, participant_id: int, slot_number: Literal[1, 2]) -> None:
|
||||
self._participant_id = participant_id
|
||||
if slot_number not in (-1, 1, 2):
|
||||
if slot_number not in (1, 2):
|
||||
raise TournamentError("Invalid slot number")
|
||||
self.slot_number = slot_number
|
||||
|
||||
@@ -49,14 +49,6 @@ class Match:
|
||||
games.append(Game(game_id, self._match_id, game_number, None, None, False))
|
||||
return tuple(games)
|
||||
|
||||
@property
|
||||
def round_number(self) -> int:
|
||||
return self._round_number
|
||||
|
||||
@property
|
||||
def best_of(self) -> int:
|
||||
return self._best_of
|
||||
|
||||
@property
|
||||
def status(self) -> MatchStatus:
|
||||
if self._status == MatchStatus.COMPLETED:
|
||||
@@ -107,9 +99,7 @@ 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, 1, 2]) -> None:
|
||||
if slot == -1:
|
||||
raise TournamentError("Normal match does not support slot -1")
|
||||
def assign_participant(self, participant_id: int, slot: Literal[1, 2]) -> None:
|
||||
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:
|
||||
@@ -141,28 +131,3 @@ 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}>")
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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()
|
||||
@@ -1,37 +0,0 @@
|
||||
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
|
||||
@@ -1,14 +1,12 @@
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from math import ceil, log2
|
||||
|
||||
from src.ezgg_lan_manager.types.Match import Match, FFAMatch
|
||||
from src.ezgg_lan_manager.types.Match import Match
|
||||
from src.ezgg_lan_manager.types.Participant import Participant
|
||||
from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, TournamentError, Bracket, MatchStatus, ParticipantType
|
||||
from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, TournamentError, Bracket, MatchStatus
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
class Tournament:
|
||||
def __init__(self,
|
||||
@@ -22,8 +20,7 @@ class Tournament:
|
||||
participants: list[Participant],
|
||||
matches: Optional[tuple[Match]],
|
||||
rounds: list[list[Match]],
|
||||
max_participants: int,
|
||||
participant_type: ParticipantType) -> None:
|
||||
max_participants: int) -> None:
|
||||
self._id = id_
|
||||
self._name = name
|
||||
self._description = description
|
||||
@@ -35,7 +32,6 @@ class Tournament:
|
||||
self._matches = matches
|
||||
self._rounds = rounds
|
||||
self._max_participants = max_participants
|
||||
self._participant_type = participant_type
|
||||
|
||||
@property
|
||||
def id(self) -> int:
|
||||
@@ -89,23 +85,11 @@ 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:
|
||||
@@ -140,12 +124,10 @@ 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") or fmt.name.endswith("FFA"):
|
||||
if fmt.name.endswith("_BO_1"):
|
||||
bo = 1
|
||||
elif fmt.name.endswith("_BO_3"):
|
||||
bo = 3
|
||||
@@ -163,28 +145,7 @@ class Tournament:
|
||||
num_participants = len(self.participants)
|
||||
match_id_counter = 1
|
||||
|
||||
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":
|
||||
if bracket_type == "SINGLE":
|
||||
# --- single-elimination as before ---
|
||||
num_rounds = ceil(log2(num_participants))
|
||||
rounds: list[list[Match]] = []
|
||||
@@ -355,7 +316,6 @@ class Tournament:
|
||||
raise TournamentError(f"Unknown bracket type: {bracket_type}")
|
||||
|
||||
self._status = TournamentStatus.ONGOING
|
||||
logger.info(f"New tournament status for {self._name}: {self._status}")
|
||||
for match in self._matches:
|
||||
match.check_completion()
|
||||
|
||||
|
||||
@@ -16,10 +16,9 @@ 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/FFA string and idx 1 is match count """
|
||||
""" Returns tuple where idx 0 is SE/DE 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:
|
||||
@@ -32,8 +31,6 @@ 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,7 +9,6 @@ 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]
|
||||
@@ -20,9 +19,4 @@ class User:
|
||||
last_updated_at: datetime
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.user_id)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, User):
|
||||
return NotImplemented
|
||||
return self.user_id == other.user_id
|
||||
return hash(f"{self.user_id}{self.user_name}{self.user_mail}")
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
from uuid import UUID
|
||||
|
||||
from rio import Dataclass
|
||||
|
||||
|
||||
class UserSession(Dataclass):
|
||||
id: UUID
|
||||
user_id: int
|
||||
is_team_member: bool
|
||||
@@ -64,19 +64,4 @@ 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)
|
||||
|
||||
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))
|
||||
self.assertIsNone(sm.winner)
|
||||
@@ -1 +0,0 @@
|
||||
*.json
|
||||
@@ -1,43 +0,0 @@
|
||||
# Tournament data
|
||||
|
||||
This directory contains JSON files for tournament trees.
|
||||
|
||||
This is a temporary solution until the automatic tournament tree generation is completed.
|
||||
|
||||
# Structure
|
||||
|
||||
## Naming
|
||||
|
||||
Tournament name with `_` as separators and `.json` suffix.
|
||||
|
||||
## JSON structure
|
||||
|
||||
```json
|
||||
{
|
||||
"rounds": [
|
||||
[
|
||||
{
|
||||
"opponent_1_id": 1,
|
||||
"opponent_2_id": 2,
|
||||
"winner": 1
|
||||
},
|
||||
{
|
||||
"opponent_1_id": 3,
|
||||
"opponent_2_id": 4,
|
||||
"winner": null
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"opponent_1_id": 1,
|
||||
"opponent_2_id": null,
|
||||
"winner": null
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## ToDo
|
||||
|
||||
- Make start button in UI generate initial `.json` file for started tournament
|
||||