26 Commits

Author SHA1 Message Date
tcprod d3bb27a03e add patches to README.md and comment to patch 2026-02-16 18:01:21 +01:00
tcprod c1a8523404 Merge remote-tracking branch 'refs/remotes/origin/bugfix/password-reset'
# Conflicts:
#	README.md
#	VERSION
#	src/EzggLanManager.py
#	src/ezgg_lan_manager/pages/TournamentDetailsPage.py
#	src/ezgg_lan_manager/pages/__init__.py
2026-02-16 17:58:16 +01:00
Typhus aec0734937 Add Teams (#45)
Co-authored-by: David Rodenkirchen <drodenkirchen@linetco.com>
Reviewed-on: #45
2026-02-15 00:16:55 +00:00
tcprod 81671f8ab2 add patches to README.md and comment to patch 2026-02-12 17:37:46 +01:00
David Rodenkirchen cd204da601 update event description 2026-02-12 09:28:42 +01:00
David Rodenkirchen 82cda92bfc Release Version 0.2.2 2026-02-11 22:35:16 +00:00
David Rodenkirchen 1ef31b5abe Fix tournament register button not updating state correctly 2026-02-11 22:35:16 +00:00
Typhus 2c53b8ef2d Add Sponsoring section to navigation (#40)
Co-authored-by: David Rodenkirchen <drodenkirchen@linetco.com>
Reviewed-on: #40
2026-02-11 22:35:16 +00:00
David Rodenkirchen 6e235bde0f bump to version 0.2.1 2026-02-11 22:35:16 +00:00
tcprod 189d785180 fix formatting total balance bug in email 2026-02-11 22:35:16 +00:00
tcprod caa992432c fix formatting balance bug in email 2026-02-11 22:35:16 +00:00
David Rodenkirchen c68273a96b Fix bug where users without ticket could register for tournament 2026-02-11 22:35:16 +00:00
David Rodenkirchen c602ce9f38 Add easteregg 2026-02-11 22:35:16 +00:00
tcprod ca58f9d74c fix reset password 2026-02-11 08:05:15 +00:00
tcprod 0b9c073900 fix reset password 2026-02-11 08:05:15 +00:00
tcprod 5a45af4465 fix reset password 2026-02-11 08:05:15 +00:00
David Rodenkirchen 24866966f4 bump to version 0.2.1 2026-02-08 00:43:03 +00:00
tcprod c73755f3b5 fix formatting total balance bug in email 2026-02-08 00:40:58 +00:00
tcprod baaa438e5e fix formatting balance bug in email 2026-02-08 00:40:58 +00:00
Typhus 19ce41bf60 Release 0.2.0
Reviewed-on: #34
2026-02-03 23:04:39 +00:00
Typhus 3a3ab7ccf4 Merge pull request 'merge main into prod' (#30) from main into prod
Reviewed-on: #30
2025-11-12 22:38:26 +00:00
David Rodenkirchen 8ce056b023 Merge branch 'main' into prod 2025-07-30 13:52:17 +02:00
David Rodenkirchen dd2ec79d1f rename lan 2025-07-26 12:23:26 +00:00
David Rodenkirchen ce881670fc upgrade rio to 0.11.2rc6 2025-07-26 12:23:26 +00:00
David Rodenkirchen bee1c9870d make portrait mode optional 2025-07-26 12:23:26 +00:00
Typhus fa3b3e77d4 Merge pull request 'prod update 03.04.25' (#19) from main into prod
Reviewed-on: Vereins-IT/ez-lan-manager#19
2025-04-03 20:55:26 +00:00
49 changed files with 404 additions and 2078 deletions
+3 -6
View File
@@ -8,16 +8,14 @@ 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.
After the database is created, apply the SQL patches located in `sql/*patch.sql` to support the fallback password- and tournament functionality.
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 +44,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.
+1 -1
View File
@@ -1 +1 @@
0.4.0
0.2.1
+1 -3
View File
@@ -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:
BIN
View File
Binary file not shown.
-10
View File
@@ -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;
File diff suppressed because one or more lines are too long
+10 -38
View File
@@ -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()
@@ -162,12 +157,6 @@ if __name__ == "__main__":
build=pages.ManageTournamentsPage,
guard=team_guard
),
ComponentPage(
name="AdminNavigationPage",
url_segment="admin",
build=pages.AdminNavigationPage,
guard=team_guard
),
ComponentPage(
name="DbErrorPage",
url_segment="db-error",
@@ -178,21 +167,11 @@ 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",
@@ -222,14 +201,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=8000,
))
+1 -2
View File
@@ -10,7 +10,6 @@ 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
@@ -35,6 +34,6 @@ def init_services() -> tuple[AccountingService, CateringService, ConfigurationSe
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
@@ -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,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,30 +1,49 @@
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),
DesktopNavigationButton(f"Über {lan_info.name} {lan_info.iteration}", "./overview"),
@@ -41,16 +60,29 @@ class DesktopNavigation(Component):
DesktopNavigationButton("Die EZ GG e.V.", "https://ezgg-ev.de/about", open_new_tab=True),
Spacer(min_height=0.7)
]
team_navigation = [
Text("Verwaltung", align_x=0.5, margin_top=0.3, style=TextStyle(fill=Color.from_hex("F0EADE"), font_size=1.2)),
Text("Vorsichtig sein!", align_x=0.5, margin_top=0.3, style=TextStyle(fill=self.session.theme.danger_color, font_size=0.6)),
DesktopNavigationButton("News", "./manage-news", is_team_navigation=True),
DesktopNavigationButton("Benutzer", "./manage-users", is_team_navigation=True),
DesktopNavigationButton("Catering", "./manage-catering", is_team_navigation=True),
DesktopNavigationButton("Turniere", "./manage-tournaments", is_team_navigation=True),
Spacer(min_height=0.7),
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,
*nav_to_use,
Text("Unsere Sponsoren", align_x=0.5, style=TextStyle(fill=self.session.theme.hud_color, font_size=0.9), margin_bottom=0.5, margin_top=1),
NavigationSponsorBox(img_name="crackz", url="https://www.crackz.gg/"),
align_y=0
+5 -10
View File
@@ -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
@@ -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),
@@ -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,
@@ -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)
+14 -14
View File
@@ -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:
+6 -16
View File
@@ -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("./")
+20 -35
View File
@@ -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
)
+11 -33
View File
@@ -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(
+6 -7
View File
@@ -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:
+4 -4
View File
@@ -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
+10 -1
View File
@@ -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(
@@ -1,6 +1,5 @@
import logging
from datetime import datetime
from functools import partial
from typing import Optional
from from_root import from_root
@@ -11,7 +10,7 @@ from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBo
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
from src.ezgg_lan_manager.types.TournamentBase import TournamentStatus
logger = logging.getLogger(__name__.split(".")[-1])
@@ -29,10 +28,7 @@ class ManageTournamentsPage(Component):
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}")
await self.session[TournamentService].start_tournament(tournament_id)
async def on_cancel_pressed(self, tournament_id: int) -> None:
logger.info(f"Canceling tournament with ID {tournament_id}")
@@ -44,11 +40,7 @@ class ManageTournamentsPage(Component):
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}
self.cancel_options = {next(filter(lambda u: u.user_id == p.id, users)).user_name: p for p in tournament.participants}
except StopIteration as e:
logger.error(f"Error trying to find user for participant: {e}")
self.tournament_id_selected_for_participant_removal = tournament_id
@@ -78,12 +70,13 @@ class ManageTournamentsPage(Component):
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"),
Tooltip(anchor=IconButton("material/play_arrow", min_size=2, margin_right=0.5, on_press=lambda: self.on_start_pressed(tournament.id)), tip="Starten"),
Tooltip(anchor=IconButton("material/cancel_schedule_send", min_size=2, margin_right=0.5, on_press=lambda: self.on_cancel_pressed(tournament.id)), tip="Absagen"),
Tooltip(anchor=IconButton("material/person_cancel", min_size=2, on_press=lambda: self.on_remove_participant_pressed(tournament.id)), tip="Spieler entfernen"),
margin=1
)
)
return Column(
Popup(
anchor=MainViewContentBox(
@@ -95,16 +88,8 @@ class ManageTournamentsPage(Component):
font_size=1.2
),
margin_top=2,
margin_bottom=1,
align_x=0.5
),
Button(
content="Cache erneuern",
shape="rectangle",
style="colored-text",
margin_bottom=2,
align_x=0.5,
on_press=self.session[TournamentService].queue_cache_renewal
align_x=0.5
),
*tournament_rows
)
@@ -3,7 +3,7 @@ from dataclasses import field
from typing import Optional
from rio import Column, Component, event, TextStyle, Text, TextInput, ThemeContextSwitcher, Grid, \
PointerEventListener, PointerEvent, Rectangle, Color, TextInputChangeEvent, Spacer, Row, Switch, \
PointerEventListener, PointerEvent, Rectangle, CursorStyle, Color, TextInputChangeEvent, Spacer, Row, Switch, \
SwitchChangeEvent, EventHandler
from src.ezgg_lan_manager import ConfigurationService, UserService, AccountingService, SeatingService, MailingService
@@ -11,9 +11,9 @@ from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBo
from src.ezgg_lan_manager.components.NewTransactionForm import NewTransactionForm
from src.ezgg_lan_manager.components.UserEditForm import UserEditForm
from src.ezgg_lan_manager.services.AccountingService import InsufficientFundsError
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
from src.ezgg_lan_manager.types.Transaction import Transaction
from src.ezgg_lan_manager.types.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
logger = logging.getLogger(__name__.split(".")[-1])
@@ -42,7 +42,7 @@ class ClickableGridContent(Component):
grow_x=True
),
fill=Color.TRANSPARENT,
cursor="pointer"
cursor=CursorStyle.POINTER
),
on_pointer_enter=self.on_mouse_enter,
on_pointer_leave=self.on_mouse_leave,
@@ -84,11 +84,7 @@ class ManageUsersPage(Component):
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 "
+60 -84
View File
@@ -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:
+4 -8
View File
@@ -6,12 +6,11 @@ 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.SessionStorage import SessionStorage
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):
@@ -27,13 +26,10 @@ class TeamsPage(Component):
@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
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Teams")
self.session[SessionStorage].subscribe_to_logged_in_or_out_event(str(self.__class__), self.on_populate)
async def on_join_button_pressed(self, team: Team) -> None:
if self.user is None:
@@ -1,36 +1,27 @@
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
PointerEvent, Rectangle, Color
from src.ezgg_lan_manager import ConfigurationService, TournamentService, UserService, TicketingService, TeamService, RefreshService
from src.ezgg_lan_manager import ConfigurationService, TournamentService, UserService, TicketingService
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.Participant import Participant
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
current_tournament_user_list: list[User] = [] # ToDo: Integrate Teams
# State for message above register button
message: str = ""
@@ -44,41 +35,16 @@ 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)
self.current_tournament_user_list = await self.session[TournamentService].get_users_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
@@ -91,96 +57,39 @@ class TournamentDetailsPage(Component):
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.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()
try:
await self.session[TournamentService].register_user_for_tournament(self.user.user_id, self.tournament.id)
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()
except Exception as e:
self.is_success = False
self.message = f"Fehler: {e}"
self.loading = False
await self.on_populate()
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()
await self.session[TournamentService].unregister_user_from_tournament(self.user.user_id, self.tournament.id)
self.is_success = True
self.message = f"Erfolgreich abgemeldet!"
except Exception as e:
self.is_success = False
self.message = f"Fehler: {e}"
await self.update()
self.loading = False
await self.on_populate()
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 +124,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,6 +176,8 @@ 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),
@@ -318,7 +203,7 @@ class TournamentDetailsPage(Component):
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}",
f"{len(self.tournament.participants)} / {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
),
@@ -330,7 +215,7 @@ class TournamentDetailsPage(Component):
Revealer(
header=None,
content=Text(
participant_names,
"\n".join([u.user_name for u in self.current_tournament_user_list]), # ToDo: Integrate Teams
style=TextStyle(fill=self.session.theme.background_color)
),
is_open=self.participant_revealer_open,
@@ -360,39 +245,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
)
-3
View File
@@ -23,6 +23,3 @@ 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
@@ -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
@@ -16,7 +17,7 @@ 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 +63,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
@@ -449,7 +449,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 +861,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 +876,6 @@ class DatabaseService:
======================= */
tp.id AS participant_id,
tp.user_id,
tp.team_id,
tp.participant_type,
tp.seed,
tp.joined_at
@@ -911,8 +909,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,16 +922,14 @@ 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"]))] if row["user_id"] is not None else [],
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)
@@ -943,14 +937,11 @@ class DatabaseService:
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);",
"INSERT INTO tournament_participants (tournament_id, user_id, participant_type) VALUES (%s, %s, %s);",
(tournament.id, participant.id, participant.participant_type.name)
)
await conn.commit()
@@ -963,12 +954,11 @@ class DatabaseService:
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);",
"DELETE FROM tournament_participants WHERE (tournament_id = %s AND user_id = %s);",
(tournament.id, participant.id)
)
await conn.commit()
@@ -1042,8 +1032,7 @@ class DatabaseService:
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)
all_teams.append(current_team)
return all_teams
@@ -1185,18 +1174,3 @@ class DatabaseService:
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)
@@ -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,13 +1,8 @@
import json
from pprint import pprint
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
@@ -22,10 +17,6 @@ class TournamentService:
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
async def _update_cache(self) -> None:
tournaments = await self._db_service.get_all_tournaments()
for tournament in tournaments:
@@ -36,24 +27,11 @@ class TournamentService:
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:
@@ -64,16 +42,6 @@ class TournamentService:
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:
await self._update_cache()
@@ -89,56 +57,16 @@ class TournamentService:
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)
# ToDo: Write matches/round to database
self._cache_dirty = True
async def cancel_tournament(self, tournament_id: int):
tournament = await self.get_tournament_by_id(tournament_id)
if tournament:
tournament.cancel()
await self._db_service.change_tournament_status(tournament_id, tournament.status)
# ToDo: Update to database
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
-8
View File
@@ -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:
@@ -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()
-8
View File
@@ -27,11 +27,3 @@ class Team:
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
+2 -11
View File
@@ -1,4 +1,3 @@
import logging
import uuid
from datetime import datetime
from typing import Optional
@@ -6,9 +5,8 @@ from math import ceil, log2
from src.ezgg_lan_manager.types.Match import Match, FFAMatch
from src.ezgg_lan_manager.types.Participant import Participant
from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, TournamentError, Bracket, MatchStatus, 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,10 +85,6 @@ 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")
@@ -355,7 +347,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()
@@ -1,9 +0,0 @@
from uuid import UUID
from rio import Dataclass
class UserSession(Dataclass):
id: UUID
user_id: int
is_team_member: bool
-1
View File
@@ -1 +0,0 @@
*.json
-43
View File
@@ -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