43 Commits

Author SHA1 Message Date
David Rodenkirchen 1dd1e8a730 stupid shit 2026-05-09 16:19:20 +02:00
David Rodenkirchen b99d2e1944 enable optional receipt 2026-05-09 15:02:40 +02:00
David Rodenkirchen 3b16004f73 Improve User Management GUI 2026-05-07 00:21:26 +02:00
Typhus ead829d322 feature/137-improve-catering-ui (#68)
Co-authored-by: David Rodenkirchen <davidr.develop@gmail.com>
Reviewed-on: #68
2026-05-02 11:01:36 +00:00
David Rodenkirchen 285f9fa09f 0.4.0 -> 0.5.0 (POS Ordering) 2026-04-29 12:10:00 +02:00
David Rodenkirchen 61fd91d9f4 Add POS ordering 2026-04-29 12:09:39 +02:00
David Rodenkirchen 9e3d252634 Hotfix 0.4.0: Disable default account 2026-04-18 17:08:56 +02:00
David Rodenkirchen 46c6c84963 Hotfix 0.4.0: Add volume for tournament data 2026-04-18 16:44:52 +02:00
Typhus 6666e79178 Enable Team Tournaments, add Tournament Trees, implement temporary tree persistance (#66)
Co-authored-by: David Rodenkirchen <drodenkirchen@linetco.com>
Reviewed-on: #66
2026-04-18 14:42:28 +00:00
David Rodenkirchen c349fe475b Fix memory leaked caused by RefreshService 2026-04-17 09:33:19 +02:00
David Rodenkirchen d5cd05c0e5 release 0.3.7 2026-04-16 08:51:21 +02:00
David Rodenkirchen b8c1df5ff8 Disable commercial PayPal charging 2026-04-16 08:50:11 +02:00
dusker 8877de2cef Add EPC QR code to make bank transactions easier (#61)
See https://de.wikipedia.org/wiki/EPC-QR-Code#EPC-QR-Code_f%C3%BCr_%C3%9Cberweisung_erstellen for more information about the EPC coding

Co-authored-by: dusker <dusker@gmx.de>
Reviewed-on: #61
Co-authored-by: dusker <jens.graef+ezgg@posteo.de>
Co-committed-by: dusker <jens.graef+ezgg@posteo.de>
2026-04-16 06:48:46 +00:00
David Rodenkirchen bd5c142bcf Fix logout not redirecting properly 2026-04-16 07:32:35 +02:00
David Rodenkirchen e0ed3c7059 update sanitized backup 2026-04-16 07:07:00 +02:00
dusker a53e7100da Fix mariadb health check by adding the root password 2026-04-03 22:09:39 +02:00
David Rodenkirchen 2902c6a58c add sanitized production export 2026-02-24 00:54:24 +01:00
David Rodenkirchen 4541d3763f Hotfix: Remove Session override 2026-02-24 00:33:40 +01:00
David Rodenkirchen ce45c389ef fix login not working after registration 2026-02-23 23:50:13 +01:00
David Rodenkirchen edf1d70b54 Bump to Version 0.3.5 (Sessioning Rework / Catering UI Improvement) 2026-02-23 21:46:47 +01:00
David Rodenkirchen 8b02390bee Make disabled catering items clearer 2026-02-23 21:46:06 +01:00
David Rodenkirchen b47eefe615 Overhaul Sessioning 2026-02-23 15:15:39 +01:00
tcprod 57c578a44b fix password reset with fallbackpassword (#51)
Fallback-Passwort eingebaut. Inkl. SQL Patch

Co-authored-by: tcprod <tcprod@gmx.degit config --global user.email tcprod@gmx.degit config --global user.email tcprod@gmx.de>
Co-authored-by: David Rodenkirchen <typhus@ezgg-ev.de>
Reviewed-on: #51
Co-authored-by: tcprod <tcprod@noreply.localhost>
Co-committed-by: tcprod <tcprod@noreply.localhost>
2026-02-23 07:05:31 +00:00
Typhus d57f4baedd enable teams to register for tournaments (#53)
Co-authored-by: David Rodenkirchen <davidr.develop@gmail.com>
Reviewed-on: #53
2026-02-22 00:45:11 +00:00
David Rodenkirchen f4db57b2ff Various smaller improvements 2026-02-21 23:39:43 +01:00
David Rodenkirchen d83182dd7b Bugfix: Teams Feature broken if there are no teams in the DB 2026-02-20 08:44:17 +01:00
David Rodenkirchen d7df5161d4 Feature: Inform user that he needs to be logged in to purchase a ticket 2026-02-20 07:16:39 +01:00
David Rodenkirchen d5b677ab68 Rework Team UI 2026-02-20 07:06:16 +01:00
David Rodenkirchen 5b6c5d2076 Deprecation Fix: CursorStyle is deprecated 2026-02-20 07:05:09 +01:00
David Rodenkirchen a390e4bd10 Bugfix: Trying to remove participant from empty tournament leads to crash 2026-02-20 07:04:37 +01:00
David Rodenkirchen 82fc0e87a8 use rio 0.12 2026-02-16 23:23:32 +01:00
Typhus deec60347b Add Teams (#45)
Co-authored-by: David Rodenkirchen <drodenkirchen@linetco.com>
Reviewed-on: #45
2026-02-16 23:22:05 +01:00
David Rodenkirchen 908bee1e7b update event description 2026-02-16 23:22:05 +01:00
David Rodenkirchen 27d1f60e2c Release Version 0.2.2 2026-02-16 23:22:05 +01:00
David Rodenkirchen d238153a22 Fix tournament register button not updating state correctly 2026-02-16 23:22:05 +01:00
Typhus 914dd4eaa8 Add Sponsoring section to navigation (#40)
Co-authored-by: David Rodenkirchen <drodenkirchen@linetco.com>
Reviewed-on: #40
2026-02-16 23:22:05 +01:00
David Rodenkirchen e79118a0f4 bump to version 0.2.1 2026-02-16 23:22:04 +01:00
tcprod 124e1a1a06 fix formatting balance bug in email
fix formatting total balance bug in email
2026-02-16 23:22:01 +01:00
David Rodenkirchen 00019a8c0d Fix bug where users without ticket could register for tournament 2026-02-16 23:21:49 +01:00
David Rodenkirchen 4e0139fef1 Add easteregg 2026-02-16 23:21:30 +01:00
David Rodenkirchen 9e86a7655e upgrade rio to 0.11.2rc6 2026-02-16 23:15:47 +01:00
David Rodenkirchen aa3691a59f Fix bug where users without ticket could register for tournament 2026-02-08 01:26:55 +01:00
David Rodenkirchen f713443c20 Add easteregg 2026-02-04 20:23:57 +01:00
69 changed files with 3791 additions and 517 deletions
+6 -2
View File
@@ -8,13 +8,16 @@ This repository contains the code for the EZGG LAN Manager.
### Prerequisites ### Prerequisites
- Working Installation of MySQL 5 or latest MariaDB Server (`mariadb-server` for Debian-based Linux, `XAMPP` for Windows) - Working Installation of MariaDB Server (version `10.6.25` or later)
+ MySQL should work too, but there are no guarantees.
- Python 3.9 or higher - Python 3.9 or higher
- PyCharm or similar IDE (optional) - PyCharm or similar IDE (optional)
### Step 1: Preparing Database ### Step 1: Preparing Database
To prepare the database, apply the SQL file located in `sql/create_database.sql` to your database server. This is easily accomplished with the MYSQL Workbench, but it can be also done by pipeing the file into the mariadb-server executable. To prepare the database, apply the SQL file located in `sql/create_database.sql` to your database server. This is easily accomplished with the MYSQL Workbench, but it can be also done by piping the file into the mariadb-server executable.
After creating the database, apply all patches found in `sql/*_patch.sql` in their numeric order.
Optionally, you can now execute the script `create_demo_database_content.py`, found in `src/ezgg_lan_manager/helpers`. Be aware that it can be buggy sometimes, especially if you overwrite existing data. Optionally, you can now execute the script `create_demo_database_content.py`, found in `src/ezgg_lan_manager/helpers`. Be aware that it can be buggy sometimes, especially if you overwrite existing data.
@@ -43,3 +46,4 @@ FLUSH PRIVILEGES;
``` ```
3. Make sure to **NOT** use the default passwords! 3. Make sure to **NOT** use the default passwords!
4. Apply the `create_database.sql` when starting the MariaDB container for the first time. 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.2.0 0.5.2
+3 -1
View File
@@ -21,7 +21,7 @@ services:
MARIADB_USER: ezgg_lan_manager MARIADB_USER: ezgg_lan_manager
MARIADB_PASSWORD: Alkohol1 MARIADB_PASSWORD: Alkohol1
healthcheck: healthcheck:
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost"] test: ["CMD", "mariadb-admin", "ping", "-h", "localhost", "-pAlkohol1"]
interval: 5s interval: 5s
timeout: 3s timeout: 3s
retries: 5 retries: 5
@@ -30,6 +30,8 @@ services:
volumes: volumes:
- database:/var/lib/mysql - database:/var/lib/mysql
- ./sql/create_database.sql:/docker-entrypoint-initdb.d/init.sql - ./sql/create_database.sql:/docker-entrypoint-initdb.d/init.sql
- ./sql:/sql
- ./tournament_data:/opt/ezgg-lan-manager/tournament_data
volumes: volumes:
BIN
View File
Binary file not shown.
+63
View File
@@ -0,0 +1,63 @@
-- =====================================================
-- Teams
-- =====================================================
DROP TABLE IF EXISTS `team_members`;
DROP TABLE IF EXISTS `teams`;
-- -----------------------------------------------------
-- Teams table
-- -----------------------------------------------------
CREATE TABLE `teams` (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
abbreviation VARCHAR(10) NOT NULL,
join_password VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_team_name (name),
UNIQUE KEY uq_team_abbr (abbreviation)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;
-- -----------------------------------------------------
-- Team Members (Junction Table)
-- -----------------------------------------------------
CREATE TABLE `team_members` (
team_id INT NOT NULL,
user_id INT NOT NULL,
status ENUM('MEMBER','OFFICER','LEADER')
NOT NULL DEFAULT 'MEMBER',
joined_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (team_id, user_id),
CONSTRAINT fk_tm_team
FOREIGN KEY (team_id)
REFERENCES teams(id)
ON DELETE CASCADE,
CONSTRAINT fk_tm_user
FOREIGN KEY (user_id)
REFERENCES users(user_id)
ON DELETE CASCADE
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;
-- -----------------------------------------------------
-- Indexes
-- -----------------------------------------------------
CREATE INDEX idx_tm_user
ON team_members(user_id);
CREATE INDEX idx_tm_team_status
ON team_members(team_id, status);
+10
View File
@@ -0,0 +1,10 @@
-- =====================================================
-- Adds type of participant to tournament
-- =====================================================
ALTER TABLE `tournaments` ADD COLUMN `participant_type` ENUM('PLAYER','TEAM') NOT NULL DEFAULT 'PLAYER' AFTER `created_at`;
ALTER TABLE `tournament_participants`
CHANGE COLUMN `user_id` `user_id` INT(11) NULL AFTER `tournament_id`,
ADD COLUMN `team_id` INT(11) NULL AFTER `user_id`,
ADD CONSTRAINT `fk_tp_team` FOREIGN KEY (`team_id`) REFERENCES `teams` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT;
+5
View File
@@ -0,0 +1,5 @@
-- Apply this patch after using create_database.sql to extend the schema to support fallback passwords
ALTER TABLE users
ADD COLUMN user_fallback_password VARCHAR(255) DEFAULT NULL
AFTER user_password;
File diff suppressed because one or more lines are too long
+47 -8
View File
@@ -1,5 +1,5 @@
import logging import logging
from asyncio import get_event_loop from uuid import uuid4
import sys import sys
@@ -8,11 +8,10 @@ from pathlib import Path
from rio import App, Theme, Color, Font, ComponentPage, Session from rio import App, Theme, Color, Font, ComponentPage, Session
from from_root import from_root from from_root import from_root
from src.ezgg_lan_manager import pages, init_services from src.ezgg_lan_manager import pages, init_services, LocalDataService, RefreshService
from src.ezgg_lan_manager.helpers.LoggedInGuard import logged_in_guard, not_logged_in_guard, team_guard 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.services.LocalDataService import LocalData
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage from src.ezgg_lan_manager.types.UserSession import UserSession
logger = logging.getLogger("EzggLanManager") logger = logging.getLogger("EzggLanManager")
@@ -30,14 +29,20 @@ if __name__ == "__main__":
corner_radius_large=0, corner_radius_large=0,
font=Font(from_root("src/ezgg_lan_manager/assets/fonts/joystix.otf")) font=Font(from_root("src/ezgg_lan_manager/assets/fonts/joystix.otf"))
) )
default_attachments: list = [LocalData()] default_attachments: list = [LocalData(stored_session_token=None)]
default_attachments.extend(init_services()) default_attachments.extend(init_services())
lan_info = default_attachments[3].get_lan_info() lan_info = default_attachments[3].get_lan_info()
async def on_session_start(session: Session) -> None: 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) await session.set_title(lan_info.name)
session.attach(SessionStorage()) 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)
async def on_app_start(a: App) -> None: async def on_app_start(a: App) -> None:
init_result = await a.default_attachments[4].init_db_pool() init_result = await a.default_attachments[4].init_db_pool()
@@ -151,12 +156,24 @@ if __name__ == "__main__":
build=pages.ManageCateringPage, build=pages.ManageCateringPage,
guard=team_guard guard=team_guard
), ),
ComponentPage(
name="NewPosOrderPage",
url_segment="new-pos-order",
build=pages.NewPosOrderPage,
guard=team_guard
),
ComponentPage( ComponentPage(
name="ManageTournamentsPage", name="ManageTournamentsPage",
url_segment="manage-tournaments", url_segment="manage-tournaments",
build=pages.ManageTournamentsPage, build=pages.ManageTournamentsPage,
guard=team_guard guard=team_guard
), ),
ComponentPage(
name="AdminNavigationPage",
url_segment="admin",
build=pages.AdminNavigationPage,
guard=team_guard
),
ComponentPage( ComponentPage(
name="DbErrorPage", name="DbErrorPage",
url_segment="db-error", url_segment="db-error",
@@ -167,10 +184,25 @@ if __name__ == "__main__":
url_segment="tournament", url_segment="tournament",
build=pages.TournamentDetailsPage, build=pages.TournamentDetailsPage,
), ),
ComponentPage(
name="TournamentTreePage",
url_segment="tournament-tree",
build=pages.TournamentTreePage,
),
ComponentPage( ComponentPage(
name="TournamentRulesPage", name="TournamentRulesPage",
url_segment="tournament-rules", url_segment="tournament-rules",
build=pages.TournamentRulesPage, build=pages.TournamentRulesPage,
),
ComponentPage(
name="Teams",
url_segment="teams",
build=pages.TeamsPage,
),
ComponentPage(
name="ConwaysGameOfLife",
url_segment="conway",
build=pages.ConwayPage,
) )
], ],
theme=theme, theme=theme,
@@ -196,7 +228,14 @@ if __name__ == "__main__":
} }
) )
sys.exit(app.run_as_web_server( try:
app.run_as_web_server(
host="0.0.0.0", host="0.0.0.0",
port=8000, 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)
+6 -3
View File
@@ -10,15 +10,17 @@ from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
from src.ezgg_lan_manager.services.LocalDataService import LocalDataService from src.ezgg_lan_manager.services.LocalDataService import LocalDataService
from src.ezgg_lan_manager.services.MailingService import MailingService from src.ezgg_lan_manager.services.MailingService import MailingService
from src.ezgg_lan_manager.services.NewsService import NewsService 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.ReceiptPrintingService import ReceiptPrintingService
from src.ezgg_lan_manager.services.SeatingService import SeatingService from src.ezgg_lan_manager.services.SeatingService import SeatingService
from src.ezgg_lan_manager.services.TeamService import TeamService
from src.ezgg_lan_manager.services.TicketingService import TicketingService from src.ezgg_lan_manager.services.TicketingService import TicketingService
from src.ezgg_lan_manager.services.TournamentService import TournamentService from src.ezgg_lan_manager.services.TournamentService import TournamentService
from src.ezgg_lan_manager.services.UserService import UserService from src.ezgg_lan_manager.services.UserService import UserService
from src.ezgg_lan_manager.types import * from src.ezgg_lan_manager.types import *
# Inits services in the correct order # Inits services in the correct order
def init_services() -> tuple[AccountingService, CateringService, ConfigurationService, DatabaseService, MailingService, NewsService, SeatingService, TicketingService, UserService, LocalDataService, ReceiptPrintingService, TournamentService]: def init_services() -> tuple[AccountingService, CateringService, ConfigurationService, DatabaseService, MailingService, NewsService, SeatingService, TicketingService, UserService, LocalDataService, ReceiptPrintingService, TournamentService, TeamService]:
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
configuration_service = ConfigurationService(from_root("config.toml")) configuration_service = ConfigurationService(from_root("config.toml"))
db_service = DatabaseService(configuration_service.get_database_configuration()) db_service = DatabaseService(configuration_service.get_database_configuration())
@@ -32,6 +34,7 @@ def init_services() -> tuple[AccountingService, CateringService, ConfigurationSe
catering_service = CateringService(db_service, accounting_service, user_service, receipt_printing_service) catering_service = CateringService(db_service, accounting_service, user_service, receipt_printing_service)
local_data_service = LocalDataService() local_data_service = LocalDataService()
tournament_service = TournamentService(db_service, user_service) tournament_service = TournamentService(db_service, user_service)
team_service = TeamService(db_service)
refresh_service = RefreshService()
return accounting_service, catering_service, configuration_service, db_service, mailing_service, news_service, seating_service, ticketing_service, user_service, local_data_service, receipt_printing_service, tournament_service, team_service
return accounting_service, catering_service, configuration_service, db_service, mailing_service, news_service, seating_service, ticketing_service, user_service, local_data_service, receipt_printing_service, tournament_service
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

@@ -0,0 +1,36 @@
from rio import Component, Rectangle, Text, Column, Icon, TextStyle, PointerEventListener, PointerEvent
class AdminNavigationCard(Component):
icon_name: str
display_text: str
target_url: str
def on_press(self, _: PointerEvent) -> None:
if self.target_url:
self.session.navigate_to(self.target_url)
def build(self) -> Component:
return PointerEventListener(
Rectangle(
content=Column(
Icon(
self.icon_name,
min_width=3.5,
min_height=3.5,
fill="background",
margin=1
),
Text(self.display_text, style=TextStyle(fill=self.session.theme.background_color), justify="center", margin=1, margin_top=0)
),
cursor="pointer",
stroke_width=0.2,
stroke_color=self.session.theme.background_color,
hover_stroke_width=0.2,
hover_stroke_color=self.session.theme.hud_color,
min_width=10,
min_height=10,
corner_radius=0.2
),
on_press=self.on_press
)
@@ -1,38 +0,0 @@
from asyncio import sleep
from rio import Text, Component, TextStyle
class AnimatedText(Component):
def __post_init__(self) -> None:
self._display_printing: list[bool] = [False]
self.text_comp = Text("")
async def display_text(self, success: bool, text: str, speed: float = 0.06, font_size: float = 0.9) -> None:
if self._display_printing[0]:
return
else:
self._display_printing[0] = True
self.text_comp.text = ""
if success:
self.text_comp.style = TextStyle(
fill=self.session.theme.success_color,
font_size=font_size
)
for c in text:
self.text_comp.text = self.text_comp.text + c
self.text_comp.force_refresh()
await sleep(speed)
else:
self.text_comp.style = TextStyle(
fill=self.session.theme.danger_color,
font_size=font_size
)
for c in text:
self.text_comp.text = self.text_comp.text + c
self.text_comp.force_refresh()
await sleep(speed)
self._display_printing[0] = False
def build(self) -> Component:
return self.text_comp
@@ -1,29 +1,30 @@
import logging
from asyncio import create_task, sleep
from functools import partial from functools import partial
from typing import Optional, Callable from typing import Optional, Callable
from rio import Component, Row, Card, Column, Text, TextStyle, Spacer, PointerEventListener, Button from rio import Component, Row, Card, Column, Text, TextStyle, Spacer, PointerEventListener, Button, Rectangle, Popup, Icon, Color
from src.ezgg_lan_manager import ReceiptPrintingService
from src.ezgg_lan_manager.components.StatusChangePopup import StatusChangePopup
from src.ezgg_lan_manager.services.CateringService import CateringService from src.ezgg_lan_manager.services.CateringService import CateringService
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus
from src.ezgg_lan_manager.types.Seat import Seat from src.ezgg_lan_manager.types.Seat import Seat
class CateringManagementOrderDisplayStatusButton(Component): logger = logging.getLogger(__name__.split(".")[-1])
status: CateringOrderStatus
clicked_cb: Callable
def build(self) -> Component:
return Button(
content=Text(
CateringOrder.translate_order_status(self.status)
),
shape="rectangle",
on_press=partial(self.clicked_cb, self.status)
)
class CateringManagementOrderDisplay(Component): class CateringManagementOrderDisplay(Component):
order: CateringOrder order: CateringOrder
seat: Optional[Seat] seat: Optional[Seat]
clicked_cb: Callable clicked_cb: Callable
status_change_popup_open: bool = False
def reprint_order(self) -> None:
create_task(self.session[ReceiptPrintingService].print_order(self.order.customer, self.order))
def open_status_change_popup(self) -> None:
self.status_change_popup_open = True
def format_order_status(self, status: CateringOrderStatus) -> Text: def format_order_status(self, status: CateringOrderStatus) -> Text:
status_text = CateringOrder.translate_order_status(status) status_text = CateringOrder.translate_order_status(status)
@@ -36,14 +37,12 @@ class CateringManagementOrderDisplay(Component):
return Text(text=status_text, style=TextStyle(fill=color)) return Text(text=status_text, style=TextStyle(fill=color))
async def status_button_clicked(self, new_status: CateringOrderStatus) -> None: async def change_status(self, new_status: CateringOrderStatus) -> Optional[str]:
if self.order.status == CateringOrderStatus.CANCELED: await sleep(1)
return
if new_status == CateringOrderStatus.CANCELED: logger.debug(f"Status of order with ID {self.order.order_id} changing from {self.order.status} to {new_status}")
# ToDo: Hier sollten wir nochmal nachfragen ob der Bediener sich wirklich sicher ist, if self.order.status == CateringOrderStatus.CANCELED: # Can not un-cancel
# und anwarnen das eine stornierte Bestellung nicht ent-storniert werden kann. return "Stornierte Bestellungen können nicht angepasst werden"
pass
if self.order.status != new_status: if self.order.status != new_status:
if new_status == CateringOrderStatus.CANCELED: if new_status == CateringOrderStatus.CANCELED:
@@ -61,9 +60,11 @@ class CateringManagementOrderDisplay(Component):
is_delivery=self.order.is_delivery is_delivery=self.order.is_delivery
) )
self.status_change_popup_open = False
def build(self) -> Component: def build(self) -> Component:
return PointerEventListener( card = Card(
content=Card(
content=Column( content=Column(
Row( Row(
Text(f"ID: {self.order.order_id}", margin_left=0.3, margin_top=0.2, justify="center", style=TextStyle(font_size=1.2)), Text(f"ID: {self.order.order_id}", margin_left=0.3, margin_top=0.2, justify="center", style=TextStyle(font_size=1.2)),
@@ -85,19 +86,32 @@ class CateringManagementOrderDisplay(Component):
Text("Geliefert" if self.order.is_delivery else "Abgeholt", margin_right=0.3, margin_bottom=0.5), Text("Geliefert" if self.order.is_delivery else "Abgeholt", margin_right=0.3, margin_bottom=0.5),
), ),
Row( Row(
CateringManagementOrderDisplayStatusButton(CateringOrderStatus.RECEIVED, self.status_button_clicked), Rectangle(
CateringManagementOrderDisplayStatusButton(CateringOrderStatus.CANCELED, self.status_button_clicked), content=Button(
CateringManagementOrderDisplayStatusButton(CateringOrderStatus.EN_ROUTE, self.status_button_clicked) content=Text("Beleg drucken", justify="left"),
shape="rectangle",
on_press=self.reprint_order
),
stroke_width=0.1
),
Rectangle(
content=Button(
content=Text("Status ändern", justify="right"),
shape="rectangle",
on_press=self.open_status_change_popup
),
stroke_width=0.1
), ),
Row(
CateringManagementOrderDisplayStatusButton(CateringOrderStatus.READY_FOR_PICKUP, self.status_button_clicked),
CateringManagementOrderDisplayStatusButton(CateringOrderStatus.COMPLETED, self.status_button_clicked),
CateringManagementOrderDisplayStatusButton(CateringOrderStatus.DELAYED, self.status_button_clicked),
) )
), ),
color=self.session.theme.hud_color, color=self.session.theme.hud_color,
colorize_on_hover=True, colorize_on_hover=True,
margin=1 margin=1
), )
status_change_popup = StatusChangePopup(card, self.status_change_popup_open, self.change_status)
return PointerEventListener(
content=status_change_popup,
on_press=partial(self.clicked_cb, self.order) on_press=partial(self.clicked_cb, self.order)
) )
@@ -1,7 +1,6 @@
from typing import Callable from typing import Callable
from rio import Component, Row, Text, TextStyle, Color, Rectangle, CursorStyle from rio import Component, Row, Text, TextStyle, Color, Rectangle, PointerEventListener
from rio.components.pointer_event_listener import PointerEvent, PointerEventListener
from src.ezgg_lan_manager.types.CateringOrder import CateringOrderStatus, CateringOrder from src.ezgg_lan_manager.types.CateringOrder import CateringOrderStatus, CateringOrder
@@ -41,7 +40,7 @@ class CateringOrderItem(Component):
fill=self.session.theme.primary_color, fill=self.session.theme.primary_color,
hover_fill=self.session.theme.hud_color, hover_fill=self.session.theme.hud_color,
transition_time=0.1, transition_time=0.1,
cursor=CursorStyle.POINTER cursor="pointer"
), ),
on_press=lambda _: self.info_modal_cb(self.order), 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), Text(AccountingService.make_euro_string_from_decimal(self.article_price),
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)), style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
IconButton( IconButton(
icon="material/add", icon="material/add" if self.is_sensitive else "material/do_not_disturb_on_total_silence",
min_size=2, min_size=2,
color=self.session.theme.success_color, color=self.session.theme.success_color if self.is_sensitive else self.session.theme.danger_color,
style="plain-text", style="colored-text",
on_press=lambda: self.on_add_callback(self.article_id), on_press=lambda: self.on_add_callback(self.article_id),
is_sensitive=self.is_sensitive is_sensitive=self.is_sensitive
), ),
@@ -1,88 +1,58 @@
from copy import copy, deepcopy
from typing import Optional, Callable from typing import Optional, Callable
from rio import * from rio import Component, event, Spacer, Card, Column, Text, TextStyle
from src.ezgg_lan_manager import ConfigurationService, UserService, LocalDataService from src.ezgg_lan_manager.services.ConfigurationService import ConfigurationService
from src.ezgg_lan_manager.services.UserService import UserService
from src.ezgg_lan_manager.components.DesktopNavigationButton import DesktopNavigationButton from src.ezgg_lan_manager.components.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.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.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
class DesktopNavigation(Component): class DesktopNavigation(Component):
user: Optional[User] = None user: Optional[User] = None
force_login_box_refresh: list[Callable] = []
@event.on_populate @event.on_populate
async def async_init(self) -> None: async def on_populate(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: try:
# Hack-around, maybe fix in the future self.user = await self.session[UserService].get_user(self.session[UserSession].user_id)
self.force_login_box_refresh[-1]() except KeyError:
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 self.user = None
def build(self) -> Component: def build(self) -> Component:
lan_info = self.session[ConfigurationService].get_lan_info() lan_info = self.session[ConfigurationService].get_lan_info()
user_info_and_login_box = UserInfoAndLoginBox() user_info_and_login_box = UserInfoAndLoginBox(state_changed_cb=self.on_populate)
self.force_login_box_refresh.append(user_info_and_login_box.force_refresh) navigation = [
user_navigation = [
DesktopNavigationButton("News", "./news"), DesktopNavigationButton("News", "./news"),
Spacer(min_height=1), Spacer(min_height=0.7),
DesktopNavigationButton(f"Über {lan_info.name} {lan_info.iteration}", "./overview"), DesktopNavigationButton(f"Über {lan_info.name} {lan_info.iteration}", "./overview"),
DesktopNavigationButton("Ticket kaufen", "./buy_ticket"), DesktopNavigationButton("Ticket kaufen", "./buy_ticket"),
DesktopNavigationButton("Sitzplan", "./seating"), DesktopNavigationButton("Sitzplan", "./seating"),
DesktopNavigationButton("Catering", "./catering"), DesktopNavigationButton("Catering", "./catering"),
DesktopNavigationButton("Teilnehmer", "./guests"), DesktopNavigationButton("Teilnehmer", "./guests"),
DesktopNavigationButton("Teams", "./teams"),
DesktopNavigationButton("Turniere", "./tournaments"), DesktopNavigationButton("Turniere", "./tournaments"),
DesktopNavigationButton("FAQ", "./faq"), DesktopNavigationButton("FAQ", "./faq"),
DesktopNavigationButton("Regeln & AGB", "./rules-gtc"), DesktopNavigationButton("Regeln & AGB", "./rules-gtc"),
Spacer(min_height=1), Spacer(min_height=0.7),
DesktopNavigationButton("Discord", "https://discord.gg/8gTjg34yyH", open_new_tab=True), DesktopNavigationButton("Discord", "https://discord.gg/8gTjg34yyH", open_new_tab=True),
DesktopNavigationButton("Die EZ GG e.V.", "https://ezgg-ev.de/about", open_new_tab=True), DesktopNavigationButton("Die EZ GG e.V.", "https://ezgg-ev.de/about", open_new_tab=True),
DesktopNavigationButton("Kontakt", "./contact"), Spacer(min_height=0.7)
DesktopNavigationButton("Impressum & DSGVO", "./imprint"),
Spacer(min_height=1)
] ]
team_navigation = [
Text("Verwaltung", align_x=0.5, margin_top=0.3, style=TextStyle(fill=Color.from_hex("F0EADE"), font_size=1.2)),
Text("Vorsichtig sein!", align_x=0.5, margin_top=0.3, style=TextStyle(fill=self.session.theme.danger_color, font_size=0.6)),
DesktopNavigationButton("News", "./manage-news", is_team_navigation=True),
DesktopNavigationButton("Benutzer", "./manage-users", is_team_navigation=True),
DesktopNavigationButton("Catering", "./manage-catering", is_team_navigation=True),
DesktopNavigationButton("Turniere", "./manage-tournaments", is_team_navigation=True),
Spacer(min_height=1),
Revealer(
header="Normale Navigation",
content=Column(*user_navigation),
header_style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9)
)
] if self.user is not None and self.user.is_team_member else []
nav_to_use = copy(team_navigation) if self.user is not None and self.user.is_team_member else copy(user_navigation) if self.user is not None and self.user.is_team_member:
navigation.insert(0, DesktopNavigationButton("Adminbereich", "./admin", is_team_navigation=True))
return Card( return Card(
Column( 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(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), 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, user_info_and_login_box,
*nav_to_use, *navigation,
Text("Unsere Sponsoren", align_x=0.5, style=TextStyle(fill=self.session.theme.hud_color, font_size=0.9), margin_bottom=0.5, margin_top=1),
NavigationSponsorBox(img_name="crackz", url="https://www.crackz.gg/"),
align_y=0 align_y=0
), ),
color=self.session.theme.neutral_color, color=self.session.theme.neutral_color,
+12 -7
View File
@@ -1,10 +1,13 @@
from rio import Component, TextStyle, Color, TextInput, Button, Text, Rectangle, Column, Row, Spacer, \ import uuid
EventHandler
from rio import Component, TextStyle, Color, TextInput, Button, Text, Rectangle, Column, Row, Spacer, \
EventHandler, Webview
from src.ezgg_lan_manager import RefreshService
from src.ezgg_lan_manager.services.LocalDataService import LocalDataService, LocalData from src.ezgg_lan_manager.services.LocalDataService import LocalDataService, LocalData
from src.ezgg_lan_manager.services.UserService import UserService 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.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
class LoginBox(Component): class LoginBox(Component):
@@ -26,11 +29,13 @@ class LoginBox(Component):
self.password_input_is_valid = True self.password_input_is_valid = True
self.login_button_is_loading = False self.login_button_is_loading = False
self.is_account_locked = False self.is_account_locked = False
await self.session[SessionStorage].set_user_id_and_team_member_flag(user.user_id, user.is_team_member) user_session = UserSession(id=uuid.uuid4(), user_id=user.user_id, is_team_member=user.is_team_member)
token = self.session[LocalDataService].set_session(self.session[SessionStorage]) self.session.attach(user_session)
token = self.session[LocalDataService].set_session(user_session)
self.session[LocalData].stored_session_token = token self.session[LocalData].stored_session_token = token
self.session.attach(self.session[LocalData]) self.session.attach(self.session[LocalData])
self.status_change_cb() await self.status_change_cb()
await self.session[RefreshService].trigger_refresh()
else: else:
self.user_name_input_is_valid = False self.user_name_input_is_valid = False
self.password_input_is_valid = False self.password_input_is_valid = False
@@ -102,5 +107,5 @@ class LoginBox(Component):
min_width=12, min_width=12,
align_x=0.5, align_x=0.5,
margin_top=0.3, margin_top=0.3,
margin_bottom=2 margin_bottom=1.5
) )
@@ -0,0 +1,23 @@
from from_root import from_root
from rio import Component, Link, Rectangle, Image, Color
class NavigationSponsorBox(Component):
img_name: str
url: str
img_suffix: str = "png"
def build(self) -> Component:
return Link(
content=Rectangle(
content=Image(image=from_root(f"src/ezgg_lan_manager/assets/img/{self.img_name}.{self.img_suffix}"), min_width=10, min_height=10),
stroke_width=0.1,
stroke_color=Color.TRANSPARENT,
hover_stroke_width=0.1,
hover_stroke_color=self.session.theme.secondary_color,
margin=0.6,
cursor="pointer"
),
target_url=self.url,
open_in_new_tab=True
)
@@ -1,11 +1,10 @@
from decimal import Decimal from decimal import Decimal
from functools import partial
from typing import Optional, Callable from typing import Optional, Callable
from rio import Component, Column, Text, TextStyle, Button, Spacer, event from rio import Component, Column, Text, TextStyle, Button, Spacer, event
from src.ezgg_lan_manager import TicketingService from src.ezgg_lan_manager import TicketingService
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage from src.ezgg_lan_manager.types.UserSession import UserSession
class SeatingPlanInfoBox(Component): class SeatingPlanInfoBox(Component):
@@ -22,11 +21,14 @@ class SeatingPlanInfoBox(Component):
@event.on_populate @event.on_populate
async def check_ticket(self) -> None: async def check_ticket(self) -> None:
if self.session[SessionStorage].user_id: try:
user_ticket = await self.session[TicketingService].get_user_ticket(self.session[SessionStorage].user_id) user_id = self.session[UserSession].user_id
user_ticket = await self.session[TicketingService].get_user_ticket(user_id)
self.has_user_ticket = not (user_ticket is None) self.has_user_ticket = not (user_ticket is None)
self.booking_button_text = "Buchen" if self.has_user_ticket else "Ticket kaufen" self.booking_button_text = "Buchen" if self.has_user_ticket else "Ticket kaufen"
self.force_refresh() self.force_refresh()
except KeyError:
return
async def purchase_clicked(self): async def purchase_clicked(self):
if self.has_user_ticket: if self.has_user_ticket:
@@ -35,6 +37,11 @@ class SeatingPlanInfoBox(Component):
self.session.navigate_to("./buy_ticket") self.session.navigate_to("./buy_ticket")
def build(self) -> Component: def build(self) -> Component:
try:
user_id = self.session[UserSession].user_id
except KeyError:
user_id = None
if self.override_text: if self.override_text:
return Column(Text(self.override_text, margin=1, return Column(Text(self.override_text, margin=1,
style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.4), overflow="wrap", style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.4), overflow="wrap",
@@ -75,7 +82,7 @@ class SeatingPlanInfoBox(Component):
grow_y=False, grow_y=False,
is_sensitive=not self.is_booking_blocked, is_sensitive=not self.is_booking_blocked,
on_press=self.purchase_clicked on_press=self.purchase_clicked
) if self.session[SessionStorage].user_id else Text(f"Du musst eingeloggt sein um einen Sitzplatz zu buchen", ) if user_id is not None else Text(f"Du musst eingeloggt sein um einen Sitzplatz zu buchen",
margin=1, margin=1,
style=TextStyle(fill=self.session.theme.neutral_color), style=TextStyle(fill=self.session.theme.neutral_color),
overflow="wrap", justify="center"), overflow="wrap", justify="center"),
@@ -4,7 +4,7 @@ from rio import Component, Text, Icon, TextStyle, Rectangle, Spacer, Color, Poin
from typing import Optional, Callable, Literal from typing import Optional, Callable, Literal
from src.ezgg_lan_manager.types.Seat import Seat from src.ezgg_lan_manager.types.Seat import Seat
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage from src.ezgg_lan_manager.types.UserSession import UserSession
class SeatPixel(Component): class SeatPixel(Component):
@@ -14,7 +14,11 @@ class SeatPixel(Component):
seat_orientation: Literal["top", "bottom"] seat_orientation: Literal["top", "bottom"]
def determine_color(self) -> Color: def determine_color(self) -> Color:
if self.seat.user is not None and self.seat.user.user_id == self.session[SessionStorage].user_id: try:
user_id = self.session[UserSession].user_id
except KeyError:
user_id = None
if self.seat.user is not None and self.seat.user.user_id == user_id:
return Color.from_hex("800080") return Color.from_hex("800080")
elif self.seat.is_blocked or self.seat.user is not None: elif self.seat.is_blocked or self.seat.user is not None:
return self.session.theme.danger_color return self.session.theme.danger_color
@@ -1,15 +1,15 @@
from asyncio import sleep, create_task from asyncio import sleep, create_task
from decimal import Decimal from decimal import Decimal
from typing import Optional
import rio from rio import Component, Column, Text, TextStyle, Button, Row, ScrollContainer, Spacer, Popup, Table, event, Card
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.CateringCartItem import CateringCartItem
from src.ezgg_lan_manager.components.CateringOrderItem import CateringOrderItem from src.ezgg_lan_manager.components.CateringOrderItem import CateringOrderItem
from src.ezgg_lan_manager.services.AccountingService import AccountingService 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.services.CateringService import CateringService, CateringError, CateringErrorType
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringMenuItemsWithAmount from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringMenuItemsWithAmount
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage from src.ezgg_lan_manager.types.UserSession import UserSession
POPUP_CLOSE_TIMEOUT_SECONDS = 3 POPUP_CLOSE_TIMEOUT_SECONDS = 3
@@ -23,16 +23,21 @@ class ShoppingCartAndOrders(Component):
@event.periodic(5) @event.periodic(5)
async def periodic_refresh_of_orders(self) -> None: async def periodic_refresh_of_orders(self) -> None:
if not self.show_cart and not self.popup_is_shown: user_id = self._get_user_id()
self.orders = await self.session[CateringService].get_orders_for_user(self.session[SessionStorage].user_id) if not self.show_cart and not self.popup_is_shown and user_id is not None:
self.orders = await self.session[CateringService].get_orders_for_user(user_id)
async def switch(self) -> None: async def switch(self) -> None:
self.show_cart = not self.show_cart self.show_cart = not self.show_cart
self.orders = await self.session[CateringService].get_orders_for_user(self.session[SessionStorage].user_id) user_id = self._get_user_id()
if user_id is not None:
self.orders = await self.session[CateringService].get_orders_for_user(user_id)
async def on_remove_item(self, list_id: int) -> None: async def on_remove_item(self, list_id: int) -> None:
catering_service = self.session[CateringService] catering_service = self.session[CateringService]
user_id = self.session[SessionStorage].user_id user_id = self._get_user_id()
if user_id is None:
return
cart = catering_service.get_cart(user_id) cart = catering_service.get_cart(user_id)
try: try:
cart.pop(list_id) cart.pop(list_id)
@@ -42,13 +47,16 @@ class ShoppingCartAndOrders(Component):
self.force_refresh() self.force_refresh()
async def on_empty_cart_pressed(self) -> None: async def on_empty_cart_pressed(self) -> None:
self.session[CateringService].save_cart(self.session[SessionStorage].user_id, []) user_id = self._get_user_id()
if user_id is None:
return
self.session[CateringService].save_cart(user_id, [])
self.force_refresh() self.force_refresh()
async def on_add_item(self, article_id: int) -> None: async def on_add_item(self, article_id: int) -> None:
catering_service = self.session[CateringService] catering_service = self.session[CateringService]
user_id = self.session[SessionStorage].user_id user_id = self._get_user_id()
if not user_id: if user_id is None:
return return
cart = catering_service.get_cart(user_id) cart = catering_service.get_cart(user_id)
item_to_add = await catering_service.get_menu_item_by_id(article_id) item_to_add = await catering_service.get_menu_item_by_id(article_id)
@@ -69,7 +77,9 @@ class ShoppingCartAndOrders(Component):
self.order_button_loading = True self.order_button_loading = True
self.force_refresh() self.force_refresh()
user_id = self.session[SessionStorage].user_id user_id = self._get_user_id()
if user_id is None:
return
cart = self.session[CateringService].get_cart(user_id) cart = self.session[CateringService].get_cart(user_id)
show_popup_task = None show_popup_task = None
if len(cart) < 1: if len(cart) < 1:
@@ -91,13 +101,13 @@ class ShoppingCartAndOrders(Component):
else: else:
show_popup_task = create_task(self.show_popup("Unbekannter Fehler", True)) show_popup_task = create_task(self.show_popup("Unbekannter Fehler", True))
else: else:
self.session[CateringService].save_cart(self.session[SessionStorage].user_id, []) self.session[CateringService].save_cart(user_id, [])
self.order_button_loading = False self.order_button_loading = False
if not show_popup_task: if not show_popup_task:
show_popup_task = create_task(self.show_popup("Bestellung erfolgreich aufgegeben!", False)) show_popup_task = create_task(self.show_popup("Bestellung erfolgreich aufgegeben!", False))
async def _create_order_info_modal(self, order: CateringOrder) -> None: async def _create_order_info_modal(self, order: CateringOrder) -> None:
def build_dialog_content() -> rio.Component: def build_dialog_content() -> Component:
# @todo: rio 0.10.8 did not have the ability to align the columns, check back in a future version # @todo: rio 0.10.8 did not have the ability to align the columns, check back in a future version
table = Table( table = Table(
{ {
@@ -107,9 +117,9 @@ class ShoppingCartAndOrders(Component):
}, },
show_row_numbers=False show_row_numbers=False
) )
return rio.Card( return Card(
rio.Column( Column(
rio.Text( Text(
f"Deine Bestellung ({order.order_id})", f"Deine Bestellung ({order.order_id})",
align_x=0.5, align_x=0.5,
margin_bottom=0.5 margin_bottom=0.5
@@ -134,10 +144,16 @@ class ShoppingCartAndOrders(Component):
) )
await dialog.wait_for_close() await dialog.wait_for_close()
def build(self) -> rio.Component: def _get_user_id(self) -> Optional[int]:
user_id = self.session[SessionStorage].user_id try:
return self.session[UserSession].user_id
except KeyError:
return None
def build(self) -> Component:
user_id = self._get_user_id()
catering_service = self.session[CateringService] catering_service = self.session[CateringService]
cart = catering_service.get_cart(user_id) cart = catering_service.get_cart(user_id) if user_id is not None else []
if self.show_cart: if self.show_cart:
cart_container = ScrollContainer( cart_container = ScrollContainer(
content=Column( content=Column(
@@ -155,7 +171,6 @@ class ShoppingCartAndOrders(Component):
margin=1 margin=1
) )
return Column( return Column(
cart_container,
Popup( Popup(
anchor=cart_container, 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), content=Text(self.popup_message, style=TextStyle(fill=self.session.theme.danger_color if self.popup_is_error else self.session.theme.success_color), overflow="wrap", margin=2, justify="center", min_width=20),
@@ -0,0 +1,92 @@
from functools import partial
from typing import Callable, Optional
from rio import Column, Row, Text, Button, Component, Icon, Popup, Rectangle, Color, Tooltip, PointerEventListener, PointerEvent, ProgressCircle
from src.ezgg_lan_manager.types.CateringOrder import CateringOrderStatus, CateringOrder
ICONS_BY_STATUS = {
CateringOrderStatus.RECEIVED: "material/move_to_inbox",
CateringOrderStatus.DELAYED: "material/hourglass_top",
CateringOrderStatus.READY_FOR_PICKUP: "material/takeout_dining",
CateringOrderStatus.EN_ROUTE: "material/local_shipping",
CateringOrderStatus.COMPLETED: "material/check_circle",
CateringOrderStatus.CANCELED: "material/cancel",
}
class StatusChangeButton(Component):
status: CateringOrderStatus
clicked_cb: Callable
def build(self) -> Component:
return Tooltip(
anchor=PointerEventListener(
content=Rectangle(
fill=Color.TRANSPARENT,
content=Column(
Icon(
icon=ICONS_BY_STATUS[self.status]
)
),
stroke_width=0.1,
stroke_color=Color.TRANSPARENT,
hover_stroke_width=0.1,
hover_stroke_color=Color.BLACK
),
on_press=partial(self.clicked_cb, self.status)
),
tip=Text(text=CateringOrder.translate_order_status(self.status)),
position="top"
)
class StatusChangePopup(Component):
anchor: Component
popup_open: bool
status_should_change_cb: Callable
response: Optional[str] = None
is_loading: bool = False
async def handle_button_clicked(self, status: CateringOrderStatus, _: PointerEvent) -> None:
self.is_loading = True
self.response = await self.status_should_change_cb(status)
self.is_loading = False
def close(self) -> None:
self.popup_open = False
def build(self) -> Component:
if self.is_loading:
content = Row(
ProgressCircle(margin=1)
)
elif self.response:
content = Row(
Text(text=self.response, justify="center", overflow="wrap", margin=1)
)
else:
content = Row(
StatusChangeButton(CateringOrderStatus.RECEIVED, self.handle_button_clicked),
StatusChangeButton(CateringOrderStatus.DELAYED, self.handle_button_clicked),
StatusChangeButton(CateringOrderStatus.READY_FOR_PICKUP, self.handle_button_clicked),
StatusChangeButton(CateringOrderStatus.EN_ROUTE, self.handle_button_clicked),
StatusChangeButton(CateringOrderStatus.COMPLETED, self.handle_button_clicked),
StatusChangeButton(CateringOrderStatus.CANCELED, self.handle_button_clicked),
spacing=0.5,
margin=0.5
)
return Popup(
anchor=self.anchor,
content=Rectangle(
content=Column(
content,
Button(content=Text(text="Abbrechen", justify="center", fill=self.session.theme.secondary_color), shape="rectangle", style="colored-text", on_press=self.close),
proportions=[2.5, 1]
),
fill=self.session.theme.hud_color,
min_width=34,
min_height=8.3
),
is_open=self.popup_open
)
@@ -0,0 +1,44 @@
from functools import partial
from typing import Callable, Optional, Literal
from rio import Component, Revealer, TextStyle, Column, Row, Tooltip, Icon, Spacer, Text, Button
from src.ezgg_lan_manager.types.Team import TeamStatus, Team
from src.ezgg_lan_manager.types.User import User
class TeamRevealer(Component):
user: Optional[User]
team: Team
mode: Literal["join", "leave", "display"]
on_button_pressed: Callable
def build(self) -> Component:
return Revealer(
header=self.team.name,
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1
),
content=Column(
*[Row(
Tooltip(
anchor=Icon("material/star" if self.team.members[member] == TeamStatus.LEADER else "material/stat_1", fill=self.session.theme.hud_color),
tip="Leiter" if self.team.members[member] == TeamStatus.LEADER else "Mitglied", position="top"),
Text(member.user_name, style=TextStyle(fill=self.session.theme.background_color, font_size=1), margin_left=0.5),
Spacer(grow_y=False))
for member in self.team.members
],
Row(Button(
content=f"{self.team.name} beitreten" if self.mode == "join" else f"{self.team.name} verlassen",
shape="rectangle",
style="major",
color="hud",
on_press=partial(self.on_button_pressed, self.team),
), margin_top=1, margin_bottom=1),
margin_right=1,
margin_left=1
),
margin_left=1,
margin_right=1,
)
@@ -0,0 +1,222 @@
import logging
from typing import Optional, Callable
from rio import Component, Text, Spacer, Rectangle, Column, TextStyle, Row, Button, TextInput, ThemeContextSwitcher
from src.ezgg_lan_manager.services.TeamService import TeamService, NotMemberError, TeamLeadRemovalError, AlreadyMemberError, NameNotAllowedError, TeamNameTooLongError, \
TeamAbbrInvalidError, TeamNameAlreadyTaken
from src.ezgg_lan_manager.types.Team import Team
from src.ezgg_lan_manager.types.User import User
logger = logging.getLogger(__name__.split(".")[-1])
class ErrorBox(Component):
error_message: str
cancel: Callable
def build(self) -> Component:
return Rectangle(
content=Column(
Text(self.error_message, style=TextStyle(fill=self.session.theme.background_color), margin_bottom=1.5),
Row(
Button(
content="Ok",
shape="rectangle",
style="major",
color="hud",
on_press=self.cancel,
)
),
margin=1
),
fill=self.session.theme.primary_color
)
class TeamsDialogJoinHandler(Component):
is_active: bool
cancel: Callable
user: Optional[User] = None
team: Optional[Team] = None
error_message: Optional[str] = None
password: str = ""
async def join(self) -> None:
if self.user is None or self.team is None:
return
if self.password != self.team.join_password:
self.error_message = "Falsches Passwort!"
return
try:
await self.session[TeamService].add_member_to_team(self.team, self.user)
except AlreadyMemberError:
self.error_message = "Du bist bereits Mitglied dieses Teams"
else:
await self.cancel_with_reset()
async def cancel_with_reset(self) -> None:
await self.cancel()
self.error_message = None
self.password = ""
def build(self) -> Component:
if not self.is_active or self.user is None or self.team is None:
return Spacer()
if self.error_message is not None:
return ErrorBox(error_message=self.error_message, cancel=self.cancel_with_reset)
return Rectangle(
content=Column(
Text(f"Team {self.team.name} beitreten", style=TextStyle(fill=self.session.theme.background_color), margin_bottom=1, justify="center"),
ThemeContextSwitcher(content=TextInput(text=self.bind().password, label="Beitrittspasswort", margin_bottom=1), color="secondary"),
Row(
Button(
content="Abbrechen",
shape="rectangle",
style="major",
color=self.session.theme.danger_color,
on_press=self.cancel_with_reset,
),
Button(
content="Beitreten",
shape="rectangle",
style="major",
color=self.session.theme.success_color,
on_press=self.join,
),
spacing=1
),
margin=1
),
fill=self.session.theme.primary_color
)
class TeamsDialogLeaveHandler(Component):
is_active: bool
cancel: Callable
user: Optional[User] = None
team: Optional[Team] = None
error_message: Optional[str] = None
async def leave(self) -> None:
if self.user is not None and self.team is not None:
try:
await self.session[TeamService].remove_member_from_team(self.team, self.user)
except NotMemberError:
self.error_message = "Du bist kein Mitglied in diesem Team"
except TeamLeadRemovalError:
self.error_message = "Als Teamleiter kannst du das Team nicht verlassen"
else:
await self.cancel_with_reset()
async def cancel_with_reset(self) -> None:
await self.cancel()
self.error_message = None
def build(self) -> Component:
if not self.is_active or self.user is None or self.team is None:
return Spacer()
if self.error_message is not None:
return ErrorBox(error_message=self.error_message, cancel=self.cancel_with_reset)
return Rectangle(
content=Column(
Text(f"Team {self.team.name} wirklich verlassen?", style=TextStyle(fill=self.session.theme.background_color), margin_bottom=1.5, justify="center"),
Row(
Button(
content="Nein",
shape="rectangle",
style="major",
color=self.session.theme.danger_color,
on_press=self.cancel_with_reset,
),
Button(
content="Ja",
shape="rectangle",
style="major",
color=self.session.theme.success_color,
on_press=self.leave,
),
spacing=1
),
margin=1
),
fill=self.session.theme.primary_color
)
class TeamsDialogCreateHandler(Component):
is_active: bool
cancel: Callable
user: Optional[User] = None
error_message: Optional[str] = None
team_name: str = ""
team_abbr: str = ""
team_join_password: str = ""
async def cancel_with_reset(self) -> None:
await self.cancel()
self.error_message = None
self.team_name, self.team_abbr, self.team_join_password = "", "", ""
async def create(self) -> None:
if self.user is None:
return
if not self.team_name or not self.team_abbr or not self.team_join_password:
self.error_message = "Angaben unvollständig"
return
try:
await self.session[TeamService].create_team(self.team_name, self.team_abbr, self.team_join_password, self.user)
except NameNotAllowedError as e:
self.error_message = f"Angaben ungültig. Darf kein '{e.disallowed_char}' enthalten."
except TeamNameTooLongError:
self.error_message = f"Name zu lang. Maximal {TeamService.MAX_TEAM_NAME_LENGTH} Zeichen."
except TeamAbbrInvalidError:
self.error_message = f"Name zu lang. Maximal {TeamService.MAX_TEAM_ABBR_LENGTH} Zeichen."
except TeamNameAlreadyTaken:
self.error_message = "Ein Team mit diesem Namen existiert bereits."
else:
await self.cancel_with_reset()
def build(self) -> Component:
if not self.is_active or self.user is None:
return Spacer()
if self.error_message is not None:
return ErrorBox(error_message=self.error_message, cancel=self.cancel_with_reset)
return Rectangle(
content=Column(
Text(f"Team gründen", style=TextStyle(fill=self.session.theme.background_color), margin_bottom=1.5, justify="center"),
ThemeContextSwitcher(content=TextInput(text=self.bind().team_name, label="Team Name", margin_bottom=1), color="secondary"),
ThemeContextSwitcher(content=TextInput(text=self.bind().team_abbr, label="Team Abkürzung", margin_bottom=1), color="secondary"),
ThemeContextSwitcher(content=TextInput(text=self.bind().team_join_password, label="Beitrittspasswort", margin_bottom=1), color="secondary"),
Row(
Button(
content="Abbrechen",
shape="rectangle",
style="major",
color=self.session.theme.danger_color,
on_press=self.cancel_with_reset,
),
Button(
content="Gründen",
shape="rectangle",
style="major",
color=self.session.theme.success_color,
on_press=self.create,
),
spacing=1
),
margin=1
),
fill=self.session.theme.primary_color
)
@@ -2,7 +2,6 @@ from functools import partial
from typing import Callable, Optional from typing import Callable, Optional
from decimal import Decimal from decimal import Decimal
import rio
from rio import Component, Card, Column, Text, Row, Button, TextStyle, ProgressBar, event, Spacer from rio import Component, Card, Column, Text, Row, Button, TextStyle, ProgressBar, event, Spacer
from src.ezgg_lan_manager import TicketingService from src.ezgg_lan_manager import TicketingService
@@ -22,10 +21,10 @@ class TicketBuyCard(Component):
available_tickets: int = 0 available_tickets: int = 0
@event.on_populate @event.on_populate
async def async_init(self) -> None: async def on_populate(self) -> None:
self.available_tickets = await self.session[TicketingService].get_available_tickets_for_category(self.category) self.available_tickets = await self.session[TicketingService].get_available_tickets_for_category(self.category)
def build(self) -> rio.Component: def build(self) -> Component:
ticket_description_style = TextStyle( ticket_description_style = TextStyle(
fill=self.session.theme.neutral_color, fill=self.session.theme.neutral_color,
font_size=1.2, 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.UserService import UserService, NameNotAllowedError
from src.ezgg_lan_manager.services.ConfigurationService import ConfigurationService 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.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
class UserEditForm(Component): class UserEditForm(Component):
@@ -35,7 +35,12 @@ class UserEditForm(Component):
async def on_populate(self) -> None: async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Profil bearbeiten") await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Profil bearbeiten")
if self.is_own_profile: if self.is_own_profile:
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id) try:
user_id = self.session[UserSession].user_id
except KeyError:
self.session.navigate_to("/")
else:
self.user = await self.session[UserService].get_user(user_id)
self.profile_picture = await self.session[UserService].get_profile_picture(self.user.user_id) self.profile_picture = await self.session[UserService].get_profile_picture(self.user.user_id)
else: else:
self.profile_picture = await self.session[UserService].get_profile_picture(self.user.user_id) self.profile_picture = await self.session[UserService].get_profile_picture(self.user.user_id)
@@ -1,15 +1,18 @@
import logging import logging
from typing import Callable
from rio import Component from rio import Component
from src.ezgg_lan_manager.components.LoginBox import LoginBox from src.ezgg_lan_manager.components.LoginBox import LoginBox
from src.ezgg_lan_manager.components.UserInfoBox import UserInfoBox from src.ezgg_lan_manager.components.UserInfoBox import UserInfoBox
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage from src.ezgg_lan_manager.types.UserSession import UserSession
logger = logging.getLogger(__name__.split(".")[-1]) logger = logging.getLogger(__name__.split(".")[-1])
class UserInfoAndLoginBox(Component): class UserInfoAndLoginBox(Component):
state_changed_cb: Callable
def build(self) -> Component: def build(self) -> Component:
if self.session[SessionStorage].user_id is None: try:
return LoginBox(status_change_cb=self.force_refresh) user_id = self.session[UserSession].user_id
else: return UserInfoBox(status_change_cb=self.state_changed_cb, user_id=user_id)
return UserInfoBox(status_change_cb=self.force_refresh) except KeyError:
return LoginBox(status_change_cb=self.state_changed_cb)
+14 -14
View File
@@ -6,6 +6,7 @@ from rio import Component, TextStyle, Color, Button, Text, Rectangle, Column, Ro
from src.ezgg_lan_manager.components.UserInfoBoxButton import UserInfoBoxButton from src.ezgg_lan_manager.components.UserInfoBoxButton import UserInfoBoxButton
from src.ezgg_lan_manager.services.LocalDataService import LocalData, LocalDataService 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.UserService import UserService
from src.ezgg_lan_manager.services.AccountingService import AccountingService from src.ezgg_lan_manager.services.AccountingService import AccountingService
from src.ezgg_lan_manager.services.TicketingService import TicketingService from src.ezgg_lan_manager.services.TicketingService import TicketingService
@@ -13,7 +14,7 @@ from src.ezgg_lan_manager.services.SeatingService import SeatingService
from src.ezgg_lan_manager.types.Seat import Seat from src.ezgg_lan_manager.types.Seat import Seat
from src.ezgg_lan_manager.types.Ticket import Ticket from src.ezgg_lan_manager.types.Ticket import Ticket
from src.ezgg_lan_manager.types.User import User from src.ezgg_lan_manager.types.User import User
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage from src.ezgg_lan_manager.types.UserSession import UserSession
class StatusButton(Component): class StatusButton(Component):
@@ -41,6 +42,7 @@ class StatusButton(Component):
class UserInfoBox(Component): class UserInfoBox(Component):
user_id: int
status_change_cb: EventHandler = None status_change_cb: EventHandler = None
TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9) TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
user: Optional[User] = None user: Optional[User] = None
@@ -53,31 +55,29 @@ class UserInfoBox(Component):
return choice(["Guten Tacho", "Tuten Gag", "Servus", "Moinjour", "Hallöchen", "Heyho", "Moinsen"]) return choice(["Guten Tacho", "Tuten Gag", "Servus", "Moinjour", "Hallöchen", "Heyho", "Moinsen"])
async def logout(self) -> None: async def logout(self) -> None:
await self.session[SessionStorage].clear() self.session.detach(UserSession)
self.user = None self.user = None
self.session[LocalDataService].del_session(self.session[LocalData].stored_session_token) self.session[LocalDataService].del_session(self.session[LocalData].stored_session_token)
self.session[LocalData].stored_session_token = None self.session[LocalData].stored_session_token = None
self.session.attach(self.session[LocalData]) self.session.attach(self.session[LocalData])
self.status_change_cb() if self.status_change_cb is not None:
self.session.navigate_to("/") await self.status_change_cb()
await self.session[RefreshService].trigger_refresh()
self.session.navigate_to("")
@event.on_populate @event.on_populate
async def async_init(self) -> None: async def async_init(self) -> None:
if self.session[SessionStorage].user_id: self.user = await self.session[UserService].get_user(self.user_id)
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id) if self.user is not None:
self.user_balance = await self.session[AccountingService].get_balance(self.user.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_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.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: async def update(self) -> None:
if not self.user: self.user_balance = await self.session[AccountingService].get_balance(self.user_id)
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id) self.user_ticket = await self.session[TicketingService].get_user_ticket(self.user_id)
if not self.user: self.user_seat = await self.session[SeatingService].get_user_seat(self.user_id)
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: def build(self) -> Component:
if not self.user: if not self.user:
@@ -117,5 +117,5 @@ class UserInfoBox(Component):
min_width=12, min_width=12,
align_x=0.5, align_x=0.5,
margin_top=0.3, margin_top=0.3,
margin_bottom=2 margin_bottom=1.5
) )
+16 -6
View File
@@ -3,22 +3,32 @@ from typing import Optional
from rio import URL, GuardEvent from rio import URL, GuardEvent
from src.ezgg_lan_manager.services.UserService import UserService from src.ezgg_lan_manager.services.UserService import UserService
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage from src.ezgg_lan_manager.types.UserSession import UserSession
# Guards pages against access from users that are NOT logged in # Guards pages against access from users that are NOT logged in
def logged_in_guard(event: GuardEvent) -> Optional[URL]: def logged_in_guard(event: GuardEvent) -> Optional[URL]:
if event.session[SessionStorage].user_id is None: try:
_ = event.session[UserSession].user_id
return None
except KeyError:
return URL("./") return URL("./")
# Guards pages against access from users that ARE logged in # Guards pages against access from users that ARE logged in
def not_logged_in_guard(event: GuardEvent) -> Optional[URL]: def not_logged_in_guard(event: GuardEvent) -> Optional[URL]:
if event.session[SessionStorage].user_id is not None: try:
_ = event.session[UserSession].user_id
return URL("./") return URL("./")
except KeyError:
return None
# Guards pages against access from users that are NOT logged in and NOT team members # Guards pages against access from users that are NOT logged in and NOT team members
def team_guard(event: GuardEvent) -> Optional[URL]: def team_guard(event: GuardEvent) -> Optional[URL]:
user_id = event.session[SessionStorage].user_id try:
is_team_member = event.session[SessionStorage].is_team_member user_id = event.session[UserSession].user_id
if user_id is None or not is_team_member: is_team_member = event.session[UserSession].is_team_member
if user_id and is_team_member:
return None
return URL("./")
except KeyError:
return URL("./") return URL("./")
+35 -20
View File
@@ -1,29 +1,39 @@
from decimal import Decimal from decimal import Decimal
from functools import partial
from typing import Optional from typing import Optional
from rio import Column, Component, event, Text, TextStyle, Button, Color, Revealer, Row, ProgressCircle, Link from rio import Column, Component, event, Text, TextStyle, Button, Color, Revealer, Row, ProgressCircle, Link, Image
from src.ezgg_lan_manager import ConfigurationService, UserService, AccountingService from src.ezgg_lan_manager import ConfigurationService, UserService, AccountingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox 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.Transaction import Transaction
from src.ezgg_lan_manager.types.User import User from src.ezgg_lan_manager.types.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
class AccountPage(Component): class AccountPage(Component):
user: Optional[User] = None user: Optional[User] = None
balance: Optional[Decimal] = None balance: Optional[Decimal] = None
transaction_history: list[Transaction] = list() transaction_history: list[Transaction] = list()
payment_qr_image: bytes = None
banking_info_revealer_open: bool = False banking_info_revealer_open: bool = False
paypal_info_revealer_open: bool = False paypal_info_revealer_open: bool = False
@event.on_populate @event.on_populate
async def on_populate(self) -> None: async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Guthabenkonto") await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Guthabenkonto")
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id) try:
self.balance = await self.session[AccountingService].get_balance(self.user.user_id) user_id = self.session[UserSession].user_id
self.transaction_history = await self.session[AccountingService].get_transaction_history(self.user.user_id) except KeyError:
pass
else:
self.user = await self.session[UserService].get_user(user_id)
self.balance = await self.session[AccountingService].get_balance(user_id)
self.transaction_history = await self.session[AccountingService].get_transaction_history(user_id)
self.payment_qr_image = self.session[AccountingService].make_payment_qr_image(
"Einfach Zocken Gaming Gesellschaft",
"GENODE51BIK",
"DE47517624340019856607",
f"AUFLADUNG - {self.user.user_id} - {self.user.user_name}")
async def _on_banking_info_press(self) -> None: async def _on_banking_info_press(self) -> None:
self.banking_info_revealer_open = not self.banking_info_revealer_open self.banking_info_revealer_open = not self.banking_info_revealer_open
@@ -32,7 +42,7 @@ class AccountPage(Component):
self.paypal_info_revealer_open = not self.paypal_info_revealer_open self.paypal_info_revealer_open = not self.paypal_info_revealer_open
def build(self) -> Component: def build(self) -> Component:
if not self.user and not self.balance: if not self.user or not self.payment_qr_image:
return Column( return Column(
MainViewContentBox( MainViewContentBox(
ProgressCircle( ProgressCircle(
@@ -81,6 +91,10 @@ class AccountPage(Component):
margin=0, margin=0,
margin_bottom=1, margin_bottom=1,
align_x=0.5 align_x=0.5
),
Image(self.payment_qr_image,
min_width=20,
min_height=20
) )
), ),
margin=2, margin=2,
@@ -219,19 +233,20 @@ class AccountPage(Component):
on_press=self._on_paypal_info_press on_press=self._on_paypal_info_press
), ),
paypal_info_revealer, paypal_info_revealer,
Link( # Disabled because people did not understand the fee's and kept charging 24.03 € to their accounts
content=Button( # Link(
content=Text("PAYPAL (3% Gebühr - Gewerblich)", style=TextStyle(fill=Color.from_hex("121212"), font_size=0.8), justify="center"), # content=Button(
shape="rectangle", # content=Text("PAYPAL (3% Gebühr - Gewerblich)", style=TextStyle(fill=Color.from_hex("121212"), font_size=0.8), justify="center"),
style="major", # shape="rectangle",
color="secondary", # style="major",
grow_x=True, # color="secondary",
margin=2, # grow_x=True,
margin_top=0 # margin=2,
), # margin_top=0
target_url="https://www.paypal.com/ncp/payment/89YMGVZ4S33RS", # ),
open_in_new_tab=True # target_url="https://www.paypal.com/ncp/payment/89YMGVZ4S33RS",
) # open_in_new_tab=True
# )
) )
), ),
MainViewContentBox( MainViewContentBox(
@@ -0,0 +1,44 @@
from rio import Column, Component, event, Text, TextStyle, Row
from src.ezgg_lan_manager.components.AdminNavigationCard import AdminNavigationCard
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.services.ConfigurationService import ConfigurationService
class AdminNavigationPage(Component):
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Admin")
def build(self) -> Component:
return Column(
MainViewContentBox(
Text(
text="Admin",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
)
),
MainViewContentBox(
Column(
Row(
AdminNavigationCard(icon_name="material/supervised_user_circle", display_text="Nutzer", target_url="manage-users"),
AdminNavigationCard(icon_name="material/fastfood", display_text="Catering", target_url="manage-catering"),
spacing=1
),
Row(
AdminNavigationCard(icon_name="material/text_ad", display_text="News", target_url="manage-news"),
AdminNavigationCard(icon_name="material/trophy", display_text="Turniere", target_url="manage-tournaments"),
spacing=1
),
margin=1,
spacing=1
)
),
align_y=0
)
+10 -2
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import * # type: ignore from typing import * # type: ignore
from rio import Component, event, Spacer, Card, Container, Column, Row, TextStyle, Color, Text, PageView, Button from rio import Component, event, Spacer, Card, Container, Column, Row, TextStyle, Color, Text, PageView, Button, Link
from src.ezgg_lan_manager import ConfigurationService, DatabaseService from src.ezgg_lan_manager import ConfigurationService, DatabaseService
from src.ezgg_lan_manager.components.DesktopNavigation import DesktopNavigation from src.ezgg_lan_manager.components.DesktopNavigation import DesktopNavigation
@@ -58,7 +58,15 @@ class BasePage(Component):
Row( Row(
Spacer(grow_x=True, grow_y=False), Spacer(grow_x=True, grow_y=False),
Card( Card(
content=Text(f"EZGG LAN Manager Version {self.session[ConfigurationService].APP_VERSION} © EZ GG e.V.", align_x=0.5, align_y=0.5, fill=self.session.theme.primary_color, style=TextStyle(font_size=0.5)), content=Row(
Text(f"EZGG LAN Manager Version {self.session[ConfigurationService].APP_VERSION} © EZ GG e.V.", fill=self.session.theme.primary_color, style=TextStyle(font_size=0.5)),
Text(f"-", fill=self.session.theme.primary_color, style=TextStyle(font_size=0.5), margin_left=0.5, margin_right=0.5),
Link(content=Text(f"Impressum & DSGVO", fill=self.session.theme.primary_color, style=TextStyle(font_size=0.5)), target_url="./imprint"),
Text(f"-", fill=self.session.theme.primary_color, style=TextStyle(font_size=0.5), margin_left=0.5, margin_right=0.5),
Link(content=Text(f"Kontakt", fill=self.session.theme.primary_color, style=TextStyle(font_size=0.5)), target_url="./contact"),
align_x=0.5,
align_y=0.5
),
color=self.session.theme.neutral_color, color=self.session.theme.neutral_color,
corner_radius=(0, 0, 0.5, 0.5), corner_radius=(0, 0, 0.5, 0.5),
grow_x=False, grow_x=False,
+27 -5
View File
@@ -2,14 +2,14 @@ from typing import Optional
from rio import Text, Column, TextStyle, Component, event, Button, Popup from rio import Text, Column, TextStyle, Component, event, Button, Popup
from src.ezgg_lan_manager import ConfigurationService, UserService, TicketingService from src.ezgg_lan_manager import ConfigurationService, UserService, TicketingService, RefreshService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.TicketBuyCard import TicketBuyCard from src.ezgg_lan_manager.components.TicketBuyCard import TicketBuyCard
from src.ezgg_lan_manager.services.AccountingService import InsufficientFundsError from src.ezgg_lan_manager.services.AccountingService import InsufficientFundsError
from src.ezgg_lan_manager.services.TicketingService import TicketNotAvailableError, UserAlreadyHasTicketError 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.Ticket import Ticket
from src.ezgg_lan_manager.types.User import User from src.ezgg_lan_manager.types.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
class BuyTicketPage(Component): class BuyTicketPage(Component):
@@ -19,14 +19,24 @@ class BuyTicketPage(Component):
popup_message: str = "" popup_message: str = ""
is_popup_success: bool = False is_popup_success: bool = False
is_buying_enabled: bool = False is_buying_enabled: bool = False
is_user_logged_in: bool = False
@event.on_populate @event.on_populate
async def on_populate(self) -> None: 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") await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Ticket kaufen")
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id) try:
user_id = self.session[UserSession].user_id
except KeyError:
self.user = None
else:
self.user = await self.session[UserService].get_user(user_id)
if self.user is None: # No user logged in if self.user is None: # No user logged in
self.is_buying_enabled = False self.is_buying_enabled = False
self.is_user_logged_in = False
self.user_ticket = None
else: # User is logged in else: # User is logged in
self.is_user_logged_in = True
possible_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id) possible_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id)
self.user_ticket = possible_ticket self.user_ticket = possible_ticket
if possible_ticket is not None: # User already has a ticket if possible_ticket is not None: # User already has a ticket
@@ -67,17 +77,29 @@ class BuyTicketPage(Component):
def build(self) -> Component: def build(self) -> Component:
ticket_infos = self.session[ConfigurationService].get_ticket_info() ticket_infos = self.session[ConfigurationService].get_ticket_info()
header = Text( header = Column(
Text(
"Tickets & Preise", "Tickets & Preise",
style=TextStyle( style=TextStyle(
fill=self.session.theme.background_color, fill=self.session.theme.background_color,
font_size=1.2 font_size=1.2
), ),
margin_top=2, margin_top=2,
margin_bottom=2,
align_x=0.5 align_x=0.5
),
spacing=0.2
) )
if not self.is_user_logged_in:
header.add(Text(
"Du musst eingeloggt sein\num ein Ticket zu kaufen",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.6
),
align_x=0.5
))
return Column( return Column(
MainViewContentBox( MainViewContentBox(
Column( Column(
+7 -6
View File
@@ -1,4 +1,4 @@
from typing import Optional, Callable from typing import Optional
from rio import Column, Component, event, TextStyle, Text, Spacer, Revealer, SwitcherBar, SwitcherBarChangeEvent, ProgressCircle from rio import Column, Component, event, TextStyle, Text, Spacer, Revealer, SwitcherBar, SwitcherBarChangeEvent, ProgressCircle
@@ -6,8 +6,9 @@ from src.ezgg_lan_manager import ConfigurationService, CateringService
from src.ezgg_lan_manager.components.CateringSelectionItem import CateringSelectionItem from src.ezgg_lan_manager.components.CateringSelectionItem import CateringSelectionItem
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.ShoppingCartAndOrders import ShoppingCartAndOrders 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.CateringMenuItem import CateringMenuItemCategory, CateringMenuItem
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage from src.ezgg_lan_manager.types.UserSession import UserSession
class CateringPage(Component): class CateringPage(Component):
@@ -15,9 +16,6 @@ class CateringPage(Component):
all_menu_items: Optional[list[CateringMenuItem]] = None all_menu_items: Optional[list[CateringMenuItem]] = None
shopping_cart_and_orders: list[ShoppingCartAndOrders] = [] 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 @event.on_populate
async def on_populate(self) -> None: async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Catering") await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Catering")
@@ -34,7 +32,10 @@ class CateringPage(Component):
return list(filter(lambda item: item.category == category, all_menu_items)) return list(filter(lambda item: item.category == category, all_menu_items))
def build(self) -> Component: def build(self) -> Component:
user_id = self.session[SessionStorage].user_id try:
user_id = self.session[UserSession].user_id
except KeyError:
user_id = None
if len(self.shopping_cart_and_orders) == 0: if len(self.shopping_cart_and_orders) == 0:
self.shopping_cart_and_orders.append(ShoppingCartAndOrders()) self.shopping_cart_and_orders.append(ShoppingCartAndOrders())
if len(self.shopping_cart_and_orders) > 1: 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 import ConfigurationService, UserService, MailingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox 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.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
class ContactPage(Component): class ContactPage(Component):
@@ -25,9 +25,9 @@ class ContactPage(Component):
@event.on_populate @event.on_populate
async def on_populate(self) -> None: async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Kontakt") await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Kontakt")
if self.session[SessionStorage].user_id is not None: try:
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id) self.user = await self.session[UserService].get_user(self.session[UserSession].user_id)
else: except KeyError:
self.user = None self.user = None
self.e_mail = "" if not self.user else self.user.user_mail self.e_mail = "" if not self.user else self.user.user_mail
+83
View File
@@ -0,0 +1,83 @@
from __future__ import annotations
from copy import deepcopy
from random import randint
from typing import * # type: ignore
from rio import Component, event, Column, Row, Color, Rectangle
class ConwayPage(Component):
"""
This is an Easter egg.
"""
active_generation: list[list] = []
rows: int = 36
cols: int = 20
@event.periodic(1)
async def calc_next_gen(self) -> None:
self.create_next_grid()
@event.on_populate
def prepare(self) -> None:
self.active_generation = self.create_initial_grid()
def create_initial_grid(self) -> list[list]:
grid = []
for row in range(self.rows):
grid_rows = []
for col in range(self.cols):
if randint(0, 7) == 0:
grid_rows += [1]
else:
grid_rows += [0]
grid += [grid_rows]
return grid
def create_next_grid(self) -> None:
next_grid = deepcopy(self.active_generation)
for row in range(self.rows):
for col in range(self.cols):
live_neighbors = self.get_live_neighbors(row, col, self.active_generation)
if live_neighbors < 2 or live_neighbors > 3:
next_grid[row][col] = 0
elif live_neighbors == 3 and self.active_generation[row][col] == 0:
next_grid[row][col] = 1
else:
next_grid[row][col] = self.active_generation[row][col]
self.active_generation = next_grid
def get_live_neighbors(self, row: int, col: int, grid: list[list]) -> int:
life_sum = 0
for i in range(-1, 2):
for j in range(-1, 2):
if not (i == 0 and j == 0):
life_sum += grid[((row + i) % self.rows)][((col + j) % self.cols)]
return life_sum
def grid_changing(self, next_grid: list[list]) -> bool:
for row in range(self.rows):
for col in range(self.cols):
if not self.active_generation[row][col] == next_grid[row][col]:
return True
return False
def build(self) -> Component:
rows = []
for row in self.active_generation:
rectangles = []
for cell in row:
color = Color.WHITE if cell == 1 else Color.BLACK
rectangles.append(Rectangle(fill=color, transition_time=0.3))
rows.append(Row(*rectangles))
return Column(*rows)
+1 -10
View File
@@ -1,23 +1,14 @@
from typing import Optional
from rio import Column, Component, event, Spacer from rio import Column, Component, event, Spacer
from src.ezgg_lan_manager import ConfigurationService, UserService from src.ezgg_lan_manager import ConfigurationService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.UserEditForm import UserEditForm 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): class EditProfilePage(Component):
user: Optional[User] = None
pfp: Optional[bytes] = None
@event.on_populate @event.on_populate
async def on_populate(self) -> None: async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Profil bearbeiten") 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: def build(self) -> Component:
return Column( return Column(
+1 -1
View File
@@ -27,7 +27,7 @@ class ForgotPasswordPage(Component):
user = await user_service.get_user(self.email_input.text.strip()) user = await user_service.get_user(self.email_input.text.strip())
if user is not None: if user is not None:
new_password = "".join(choices(user_service.ALLOWED_USER_NAME_SYMBOLS, k=16)) new_password = "".join(choices(user_service.ALLOWED_USER_NAME_SYMBOLS, k=16))
user.user_password = sha256(new_password.encode(encoding="utf-8")).hexdigest() user.user_fallback_password = sha256(new_password.encode(encoding="utf-8")).hexdigest()
await user_service.update_user(user) await user_service.update_user(user)
await mailing_service.send_email( await mailing_service.send_email(
subject=f"Dein neues Passwort für {lan_info.name}", subject=f"Dein neues Passwort für {lan_info.name}",
@@ -3,7 +3,7 @@ from dataclasses import field, dataclass
from datetime import datetime from datetime import datetime
from typing import Optional, Callable from typing import Optional, Callable
from rio import Column, Component, event, TextStyle, Text, Spacer, PointerEvent, Button, Popup, Card, Row from rio import Column, Component, event, TextStyle, Text, Spacer, PointerEvent, Button, Popup, Card, Row, Rectangle, Color, PointerEventListener
from src.ezgg_lan_manager import ConfigurationService, CateringService, SeatingService, AccountingService from src.ezgg_lan_manager import ConfigurationService, CateringService, SeatingService, AccountingService
from src.ezgg_lan_manager.components.CateringManagementOrderDisplay import CateringManagementOrderDisplay from src.ezgg_lan_manager.components.CateringManagementOrderDisplay import CateringManagementOrderDisplay
@@ -106,6 +106,7 @@ class ManageCateringPage(Component):
return sorted_list return sorted_list
async def order_clicked(self, order: CateringOrder, _: PointerEvent) -> None: async def order_clicked(self, order: CateringOrder, _: PointerEvent) -> None:
await self.update_orders()
self.order_popup_order = order self.order_popup_order = order
self.order_popup_open = True self.order_popup_open = True
@@ -120,7 +121,7 @@ class ManageCateringPage(Component):
font_size=1.2 font_size=1.2
), ),
margin_top=2, margin_top=2,
margin_bottom=2, margin_bottom=1,
align_x=0.5 align_x=0.5
) )
popup = Popup( popup = Popup(
@@ -132,7 +133,26 @@ class ManageCateringPage(Component):
) )
return Column( return Column(
MainViewContentBox( MainViewContentBox(
Column(popup) Column(
popup,
PointerEventListener(
content=Rectangle(
content=Text(text="Neue Bestellung anlegen", fill=Color.WHITE, justify="center", margin=0.3),
margin_bottom=1,
margin_right=5,
margin_left=5,
fill=self.session.theme.secondary_color,
hover_fill=self.session.theme.hud_color,
stroke_width=0.2,
stroke_color=Color.TRANSPARENT,
hover_stroke_width=0.2,
hover_stroke_color=self.session.theme.background_color,
cursor="pointer",
transition_time=0.1
),
on_press=lambda _: self.session.navigate_to("new-pos-order")
)
)
), ),
MainViewContentBox( MainViewContentBox(
Column( Column(
@@ -1,5 +1,6 @@
import logging import logging
from datetime import datetime from datetime import datetime
from functools import partial
from typing import Optional from typing import Optional
from from_root import from_root from from_root import from_root
@@ -10,7 +11,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.DateUtil import weekday_to_display_text
from src.ezgg_lan_manager.types.Participant import Participant from src.ezgg_lan_manager.types.Participant import Participant
from src.ezgg_lan_manager.types.Tournament import Tournament from src.ezgg_lan_manager.types.Tournament import Tournament
from src.ezgg_lan_manager.types.TournamentBase import TournamentStatus from src.ezgg_lan_manager.types.TournamentBase import TournamentStatus, TournamentError
logger = logging.getLogger(__name__.split(".")[-1]) logger = logging.getLogger(__name__.split(".")[-1])
@@ -28,7 +29,10 @@ class ManageTournamentsPage(Component):
async def on_start_pressed(self, tournament_id: int) -> None: async def on_start_pressed(self, tournament_id: int) -> None:
logger.info(f"Starting tournament with ID {tournament_id}") logger.info(f"Starting tournament with ID {tournament_id}")
try:
await self.session[TournamentService].start_tournament(tournament_id) await self.session[TournamentService].start_tournament(tournament_id)
except TournamentError as e:
logger.error(f"Error trying to start tournament: {e}")
async def on_cancel_pressed(self, tournament_id: int) -> None: async def on_cancel_pressed(self, tournament_id: int) -> None:
logger.info(f"Canceling tournament with ID {tournament_id}") logger.info(f"Canceling tournament with ID {tournament_id}")
@@ -40,7 +44,11 @@ class ManageTournamentsPage(Component):
return return
users = await self.session[UserService].get_all_users() users = await self.session[UserService].get_all_users()
try: try:
self.cancel_options = {next(filter(lambda u: u.user_id == p.id, users)).user_name: p for p in tournament.participants} cancel_options = {next(filter(lambda u: u.user_id == p.id, users)).user_name: p for p in tournament.participants}
if cancel_options:
self.cancel_options = cancel_options
else:
self.cancel_options = {"": None}
except StopIteration as e: except StopIteration as e:
logger.error(f"Error trying to find user for participant: {e}") logger.error(f"Error trying to find user for participant: {e}")
self.tournament_id_selected_for_participant_removal = tournament_id self.tournament_id_selected_for_participant_removal = tournament_id
@@ -70,13 +78,12 @@ 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(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), 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(), Spacer(),
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/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=lambda: self.on_cancel_pressed(tournament.id)), tip="Absagen"), 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=lambda: self.on_remove_participant_pressed(tournament.id)), tip="Spieler entfernen"), Tooltip(anchor=IconButton("material/person_cancel", min_size=2, on_press=partial(self.on_remove_participant_pressed, tournament.id)), tip="Spieler entfernen"),
margin=1 margin=1
) )
) )
return Column( return Column(
Popup( Popup(
anchor=MainViewContentBox( anchor=MainViewContentBox(
@@ -88,9 +95,17 @@ class ManageTournamentsPage(Component):
font_size=1.2 font_size=1.2
), ),
margin_top=2, margin_top=2,
margin_bottom=2, margin_bottom=1,
align_x=0.5 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
),
*tournament_rows *tournament_rows
) )
), ),
+38 -10
View File
@@ -3,17 +3,17 @@ from dataclasses import field
from typing import Optional from typing import Optional
from rio import Column, Component, event, TextStyle, Text, TextInput, ThemeContextSwitcher, Grid, \ from rio import Column, Component, event, TextStyle, Text, TextInput, ThemeContextSwitcher, Grid, \
PointerEventListener, PointerEvent, Rectangle, CursorStyle, Color, TextInputChangeEvent, Spacer, Row, Switch, \ PointerEventListener, PointerEvent, Rectangle, Color, TextInputChangeEvent, Spacer, Row, Switch, \
SwitchChangeEvent, EventHandler SwitchChangeEvent, EventHandler, Icon
from src.ezgg_lan_manager import ConfigurationService, UserService, AccountingService, SeatingService, MailingService from src.ezgg_lan_manager import ConfigurationService, UserService, AccountingService, SeatingService, MailingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.NewTransactionForm import NewTransactionForm from src.ezgg_lan_manager.components.NewTransactionForm import NewTransactionForm
from src.ezgg_lan_manager.components.UserEditForm import UserEditForm from src.ezgg_lan_manager.components.UserEditForm import UserEditForm
from src.ezgg_lan_manager.services.AccountingService import InsufficientFundsError 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.Transaction import Transaction
from src.ezgg_lan_manager.types.User import User from src.ezgg_lan_manager.types.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
logger = logging.getLogger(__name__.split(".")[-1]) logger = logging.getLogger(__name__.split(".")[-1])
@@ -42,7 +42,7 @@ class ClickableGridContent(Component):
grow_x=True grow_x=True
), ),
fill=Color.TRANSPARENT, fill=Color.TRANSPARENT,
cursor=CursorStyle.POINTER cursor="pointer"
), ),
on_pointer_enter=self.on_mouse_enter, on_pointer_enter=self.on_mouse_enter,
on_pointer_leave=self.on_mouse_leave, on_pointer_leave=self.on_mouse_leave,
@@ -73,18 +73,28 @@ class ManageUsersPage(Component):
seat = await self.session[SeatingService].get_user_seat(self.selected_user.user_id) seat = await self.session[SeatingService].get_user_seat(self.selected_user.user_id)
self.user_seat = seat.seat_id if seat else "-" self.user_seat = seat.seat_id if seat else "-"
self.is_user_account_locked = not self.selected_user.is_active self.is_user_account_locked = not self.selected_user.is_active
await self.on_search_parameters_changed(TextInputChangeEvent(self.selected_user.user_name))
async def on_search_parameters_changed(self, e: TextInputChangeEvent) -> None: async def on_search_parameters_changed(self, e: TextInputChangeEvent) -> None:
self.search_results = list( self.search_results = list(
filter(lambda user: (e.text.lower() in user.user_name.lower()) or e.text.lower() in str(user.user_id), filter(lambda user: (e.text.lower() in user.user_name.lower()) or e.text.lower() in str(user.user_id),
self.all_users)) self.all_users))
async def reset_view(self, _: PointerEvent) -> None:
self.selected_user = None
self.search_results = self.all_users
await self.on_search_parameters_changed(TextInputChangeEvent(""))
async def change_account_active(self, _: SwitchChangeEvent) -> None: async def change_account_active(self, _: SwitchChangeEvent) -> None:
self.selected_user.is_active = not self.is_user_account_locked self.selected_user.is_active = not self.is_user_account_locked
await self.session[UserService].update_user(self.selected_user) await self.session[UserService].update_user(self.selected_user)
async def on_new_transaction(self, transaction: Transaction) -> None: async def on_new_transaction(self, transaction: Transaction) -> None:
if not self.session[SessionStorage].is_team_member: # Better safe than sorry try:
user = await self.session[UserService].get_user(self.session[UserSession].user_id)
if not user.is_team_member: # Better safe than sorry
return
except KeyError:
return return
logger.info(f"Got new transaction for user with ID '{transaction.user_id}' over " logger.info(f"Got new transaction for user with ID '{transaction.user_id}' over "
@@ -94,7 +104,7 @@ class ManageUsersPage(Component):
if transaction.is_debit: if transaction.is_debit:
try: try:
await self.session[AccountingService].remove_balance( new_total_balance = await self.session[AccountingService].remove_balance(
transaction.user_id, transaction.user_id,
transaction.value, transaction.value,
transaction.reference transaction.reference
@@ -118,6 +128,7 @@ class ManageUsersPage(Component):
self.accounting_section_result_text = f"Guthaben {'entfernt' if transaction.is_debit else 'hinzugefügt'}!" self.accounting_section_result_text = f"Guthaben {'entfernt' if transaction.is_debit else 'hinzugefügt'}!"
self.accounting_section_result_success = True self.accounting_section_result_success = True
self.user_account_balance = self.session[AccountingService].make_euro_string_from_decimal(new_total_balance)
def build(self) -> Component: def build(self) -> Component:
return Column( return Column(
@@ -160,15 +171,32 @@ class ManageUsersPage(Component):
), ),
MainViewContentBox( MainViewContentBox(
Column( Column(
Row(
Spacer(),
PointerEventListener(
content=Icon("material/cancel", fill="background", min_width=2.5, margin_top=1, margin_right=1),
on_press=self.reset_view
)
),
Row(
Text( Text(
text="Konto & Sitzplatz", text=f"Konto von ",
style=TextStyle( style=TextStyle(
fill=self.session.theme.background_color, fill=self.session.theme.background_color,
font_size=1.2 font_size=1.2
), ),
margin_top=2, justify="right"
margin_bottom=2, ),
align_x=0.5 Text(
text=self.selected_user.user_name,
style=TextStyle(
fill=self.session.theme.hud_color,
font_size=1.2
),
justify="left"
),
margin_top=0.5,
margin_bottom=2
), ),
Row( Row(
Text( Text(
@@ -0,0 +1,423 @@
import logging
from asyncio import sleep, create_task
from decimal import Decimal
from functools import partial
from typing import Optional, Callable
from rio import Column, Component, event, TextStyle, Text, Spacer, Revealer, ProgressCircle, ScrollContainer, Row, Popup, List, Rectangle, PointerEventListener, \
PointerEvent, TextInput, TextInputChangeEvent, Color, Button, Checkbox, Switch
from src.ezgg_lan_manager import ConfigurationService, CateringService, AccountingService
from src.ezgg_lan_manager.components.CateringCartItem import CateringCartItem
from src.ezgg_lan_manager.components.CateringSelectionItem import CateringSelectionItem
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.services.CateringService import CateringError, CateringErrorType
from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItemCategory, CateringMenuItem
from src.ezgg_lan_manager.types.CateringOrder import CateringMenuItemsWithAmount
from src.ezgg_lan_manager.types.UserSession import UserSession
POPUP_CLOSE_TIMEOUT_SECONDS = 3
logger = logging.getLogger(__name__.split(".")[-1])
class Cart(Component):
cart: List[CateringMenuItem]
user_id: Optional[int]
clear_cb: Callable
order_button_loading: bool = False
popup_message: str = ""
popup_is_shown: bool = False
popup_is_error: bool = True
print_receipt: bool = True
async def on_remove_item(self, list_id: int) -> None:
try:
self.cart.pop(list_id)
except IndexError:
return
async def on_empty_cart_pressed(self, _: PointerEvent) -> None:
self.cart.clear()
async def show_popup(self, text: str, is_error: bool) -> None:
self.popup_is_error = is_error
self.popup_message = text
self.popup_is_shown = True
self.force_refresh()
await sleep(POPUP_CLOSE_TIMEOUT_SECONDS)
self.popup_is_shown = False
self.force_refresh()
async def on_order_pressed(self, _: PointerEvent) -> None:
if self.user_id is None:
return
self.order_button_loading = True
self.force_refresh()
show_popup_task = None
if len(self.cart) < 1:
show_popup_task = create_task(self.show_popup("Warenkorb leer", True))
else:
items_with_amounts: CateringMenuItemsWithAmount = {}
for item in self.cart:
try:
items_with_amounts[item] += 1
except KeyError:
items_with_amounts[item] = 1
try:
await self.session[CateringService].place_order(items_with_amounts, self.user_id, is_delivery=False, print_receipt=self.print_receipt)
except CateringError as catering_error:
logger.error(catering_error)
if catering_error.error_type == CateringErrorType.INCLUDES_DISABLED_ITEM:
show_popup_task = create_task(self.show_popup("Warenkorb enthält gesperrte Artikel", True))
elif catering_error.error_type == CateringErrorType.INSUFFICIENT_FUNDS:
show_popup_task = create_task(self.show_popup("Guthaben nicht ausreichend", True))
else:
show_popup_task = create_task(self.show_popup(f"Unbekannter Fehler: {catering_error}", True))
else:
self.cart.clear()
self.user_id = None
await self.clear_cb()
self.order_button_loading = False
if not show_popup_task:
show_popup_task = create_task(self.show_popup("Bestellung erfolgreich aufgegeben!", False))
def build(self) -> Component:
cart_container = ScrollContainer(
content=Column(
*[CateringCartItem(
article_name=cart_item.name,
article_price=cart_item.price,
article_id=cart_item.item_id,
remove_item_cb=self.on_remove_item,
list_id=idx
) for idx, cart_item in enumerate(self.cart)],
Spacer(grow_y=True)
),
min_height=8,
min_width=33,
margin=1
)
return Column(
Popup(
anchor=cart_container,
content=Text(self.popup_message, style=TextStyle(fill=self.session.theme.danger_color if self.popup_is_error else self.session.theme.success_color), overflow="wrap", margin=2, justify="center", min_width=20),
is_open=self.popup_is_shown,
position="center",
color=self.session.theme.primary_color
),
Row(
Text(
text=f"Preis: {AccountingService.make_euro_string_from_decimal(sum((cart_item.price for cart_item in self.cart), Decimal(0)))}",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.8
),
margin=1
),
PointerEventListener(
content=Rectangle(
content=Text(
"Warenkorb leeren",
style=TextStyle(fill=self.session.theme.danger_color, font_size=0.9),
justify="center",
margin=0.2
),
hover_fill=self.session.theme.hud_color,
transition_time=0.1,
margin=0.5,
cursor="pointer"
),
on_press=self.on_empty_cart_pressed
),
PointerEventListener(
content=Rectangle(
content=Text(
"Bestellen",
style=TextStyle(fill=self.session.theme.success_color, font_size=0.9),
justify="center",
margin=0.2
),
hover_fill=self.session.theme.hud_color if self.user_id is not None else self.session.theme.danger_color,
transition_time=0.1,
margin=0.5,
cursor="pointer" if self.user_id is not None else "not-allowed"
),
on_press=self.on_order_pressed
)
)
)
class NewPosOrderPage(Component):
user_id_input_value: str = ""
user_id: Optional[int] = None
all_menu_items: Optional[list[CateringMenuItem]] = None
cart: List[CateringMenuItem] = List()
print_receipt: bool = True
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Neue Bestellung anlegen")
self.all_menu_items = await self.session[CateringService].get_menu()
async def on_user_logged_in_status_changed(self) -> None:
self.force_refresh()
async def on_user_id_input_change(self, change_event: TextInputChangeEvent) -> None:
try:
id_ = int(change_event.text)
except ValueError:
return
self.user_id = id_
async def on_add(self, article_id: int) -> None:
try:
menu_item = await self.session[CateringService].get_menu_item_by_id(article_id)
except CateringError as e:
logger.error(e)
return
self.cart.append(menu_item)
@staticmethod
def get_menu_items_by_category(all_menu_items: list[CateringMenuItem], category: Optional[CateringMenuItemCategory]) -> list[CateringMenuItem]:
return list(filter(lambda item: item.category == category, all_menu_items))
async def clear_user_id_input(self) -> None:
self.user_id_input_value = ""
def build(self) -> Component:
try:
is_team_member = self.session[UserSession].is_team_member
except KeyError:
is_team_member = False
shopping_cart_container = MainViewContentBox(
Column(
Text(
text="Neue Bestellung anlegen",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=0.5,
align_x=0.5
),
Row(
TextInput(text=self.bind().user_id_input_value, label="Nutzer ID", on_change=self.on_user_id_input_change, change_delay=1, margin_bottom=0.5, margin_left=1, margin_right=1, min_width=20),
Text("Bon drucken?:", margin_right=0, margin_left=0, style=TextStyle(fill=self.session.theme.background_color)),
Switch(is_on=self.bind().print_receipt, margin_right=1)
),
Cart(cart=self.cart, user_id=self.user_id, clear_cb=self.clear_user_id_input, print_receipt=self.print_receipt)
)
) if is_team_member else Spacer()
menu = [MainViewContentBox(
ProgressCircle(
color="secondary",
align_x=0.5,
margin_top=2,
margin_bottom=2
)
)] if not self.all_menu_items else [MainViewContentBox(
Revealer(
header="Snacks",
content=Column(
*[CateringSelectionItem(
article_name=catering_menu_item.name,
article_price=catering_menu_item.price,
article_id=catering_menu_item.item_id,
on_add_callback=self.on_add,
is_sensitive=not catering_menu_item.is_disabled,
additional_info=catering_menu_item.additional_info,
is_grey=idx % 2 == 0
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.SNACK))],
),
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=1,
align_y=0.5
)
),
MainViewContentBox(
Revealer(
header="Frühstück",
content=Column(
*[CateringSelectionItem(
article_name=catering_menu_item.name,
article_price=catering_menu_item.price,
article_id=catering_menu_item.item_id,
on_add_callback=self.on_add,
is_sensitive=not catering_menu_item.is_disabled,
additional_info=catering_menu_item.additional_info,
is_grey=idx % 2 == 0
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.BREAKFAST))],
),
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=1,
align_y=0.5
)
),
MainViewContentBox(
Revealer(
header="Hauptspeisen",
content=Column(
*[CateringSelectionItem(
article_name=catering_menu_item.name,
article_price=catering_menu_item.price,
article_id=catering_menu_item.item_id,
on_add_callback=self.on_add,
is_sensitive=not catering_menu_item.is_disabled,
additional_info=catering_menu_item.additional_info,
is_grey=idx % 2 == 0
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.MAIN_COURSE))],
),
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=1,
align_y=0.5
)
),
MainViewContentBox(
Revealer(
header="Desserts",
content=Column(
*[CateringSelectionItem(
article_name=catering_menu_item.name,
article_price=catering_menu_item.price,
article_id=catering_menu_item.item_id,
on_add_callback=self.on_add,
is_sensitive=not catering_menu_item.is_disabled,
additional_info=catering_menu_item.additional_info,
is_grey=idx % 2 == 0
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.DESSERT))],
),
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=1,
align_y=0.5
)
),
MainViewContentBox(
Revealer(
header="Wasser & Softdrinks",
content=Column(
*[CateringSelectionItem(
article_name=catering_menu_item.name,
article_price=catering_menu_item.price,
article_id=catering_menu_item.item_id,
on_add_callback=self.on_add,
is_sensitive=not catering_menu_item.is_disabled,
additional_info=catering_menu_item.additional_info,
is_grey=idx % 2 == 0
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC))],
),
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=1,
align_y=0.5
)
),
MainViewContentBox(
Revealer(
header="Alkoholische Getränke",
content=Column(
*[CateringSelectionItem(
article_name=catering_menu_item.name,
article_price=catering_menu_item.price,
article_id=catering_menu_item.item_id,
on_add_callback=self.on_add,
is_sensitive=not catering_menu_item.is_disabled,
additional_info=catering_menu_item.additional_info,
is_grey=idx % 2 == 0
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC))],
),
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=1,
align_y=0.5
)
),
MainViewContentBox(
Revealer(
header="Cocktails & Longdrinks",
content=Column(
*[CateringSelectionItem(
article_name=catering_menu_item.name,
article_price=catering_menu_item.price,
article_id=catering_menu_item.item_id,
on_add_callback=self.on_add,
is_sensitive=not catering_menu_item.is_disabled,
additional_info=catering_menu_item.additional_info,
is_grey=idx % 2 == 0
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.BEVERAGE_COCKTAIL))],
),
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=1,
align_y=0.5
)
),
MainViewContentBox(
Revealer(
header="Shots",
content=Column(
*[CateringSelectionItem(
article_name=catering_menu_item.name,
article_price=catering_menu_item.price,
article_id=catering_menu_item.item_id,
on_add_callback=self.on_add,
is_sensitive=not catering_menu_item.is_disabled,
additional_info=catering_menu_item.additional_info,
is_grey=idx % 2 == 0
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.BEVERAGE_SHOT))],
),
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=1,
align_y=0.5
)
),
MainViewContentBox(
Revealer(
header="Sonstiges",
content=Column(
*[CateringSelectionItem(
article_name=catering_menu_item.name,
article_price=catering_menu_item.price,
article_id=catering_menu_item.item_id,
on_add_callback=self.on_add,
is_sensitive=not catering_menu_item.is_disabled,
additional_info=catering_menu_item.additional_info,
is_grey=idx % 2 == 0
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.NON_FOOD))],
),
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=1,
align_y=0.5
)
)]
return Column(shopping_cart_container, *menu, align_y=0)
+1 -13
View File
@@ -75,7 +75,7 @@ class OverviewPage(Component):
Row( Row(
Text("Internet", fill=self.session.theme.neutral_color, margin_left=1), Text("Internet", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(), Spacer(),
Text(f"60/20 Mbit/s (down/up)", fill=self.session.theme.neutral_color, margin_right=1), Text(f"100/50 Mbit/s (down/up)", fill=self.session.theme.neutral_color, margin_right=1),
margin_bottom=0.3 margin_bottom=0.3
), ),
Row( Row(
@@ -125,17 +125,5 @@ class OverviewPage(Component):
) )
) )
), ),
MainViewContentBox(
Column(
Text("Turniere & Ablauf", font_size=2, justify="center", fill=self.session.theme.neutral_color, margin_top=0.5, margin_bottom=1),
Column(
Row(
Text("Zum aktuellen Zeitpunkt steht noch nicht fest welche Turniere gespielt werden. Wir planen diverse Online- und Offline Turniere mit Preisen durchzuführen. Weitere Informationen gibt es, sobald sie kommen, auf der NEWS- und Turnier-Seite.", font_size=0.7,
fill=self.session.theme.neutral_color, margin_left=1, overflow="wrap"),
margin_bottom=0.3
)
)
)
),
Spacer() Spacer()
) )
+84 -60
View File
@@ -1,10 +1,10 @@
import logging import logging
from asyncio import sleep, create_task
from email_validator import validate_email, EmailNotValidError from email_validator import validate_email, EmailNotValidError
from rio import Column, Component, event, Text, TextStyle, TextInput, TextInputChangeEvent, Button 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 import ConfigurationService, UserService, MailingService
from src.ezgg_lan_manager.components.AnimatedText import AnimatedText
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
MINIMUM_PASSWORD_LENGTH = 6 MINIMUM_PASSWORD_LENGTH = 6
@@ -13,125 +13,154 @@ logger = logging.getLogger(__name__.split(".")[-1])
class RegisterPage(Component): 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: def on_pw_focus_loss(self, _: TextInputChangeEvent) -> None:
if not (self.pw_1.text == self.pw_2.text) or len(self.pw_1.text) < MINIMUM_PASSWORD_LENGTH: if not (self.pw_1 == self.pw_2) or len(self.pw_1) < MINIMUM_PASSWORD_LENGTH:
self.pw_1.is_valid = False self.pw_1_valid = False
self.pw_2.is_valid = False self.pw_2_valid = False
return return
self.pw_1.is_valid = True self.pw_1_valid = True
self.pw_2.is_valid = True self.pw_2_valid = True
def on_email_focus_loss(self, change_event: TextInputChangeEvent) -> None: def on_email_focus_loss(self, change_event: TextInputChangeEvent) -> None:
try: try:
validate_email(change_event.text, check_deliverability=False) validate_email(change_event.text, check_deliverability=False)
self.email_input.is_valid = True self.email_valid = True
except EmailNotValidError: except EmailNotValidError:
self.email_input.is_valid = False self.email_valid = False
def on_user_name_focus_loss(self, _: TextInputChangeEvent) -> None: def on_user_name_focus_loss(self, _: TextInputChangeEvent) -> None:
current_text = self.user_name_input.text current_text = self.user_name
if len(current_text) > UserService.MAX_USERNAME_LENGTH: if len(current_text) > UserService.MAX_USERNAME_LENGTH:
self.user_name_input.text = current_text[:UserService.MAX_USERNAME_LENGTH] self.user_name = current_text[:UserService.MAX_USERNAME_LENGTH]
async def on_submit_button_pressed(self) -> None: async def on_submit_button_pressed(self) -> None:
self.submit_button.is_loading = True self.submit_button_loading = True
self.submit_button.force_refresh()
if len(self.user_name_input.text) < 1: if len(self.user_name) < 1:
await self.animated_text.display_text(False, "Nutzername darf nicht leer sein!") await self.display_animated_text(False, "Nutzername darf nicht leer sein!")
self.submit_button.is_loading = False self.submit_button_loading = False
return return
if not (self.pw_1.text == self.pw_2.text): if not (self.pw_1 == self.pw_2):
await self.animated_text.display_text(False, "Passwörter stimmen nicht überein!") await self.display_animated_text(False, "Passwörter stimmen nicht überein!")
self.submit_button.is_loading = False self.submit_button_loading = False
return return
if len(self.pw_1.text) < MINIMUM_PASSWORD_LENGTH: if len(self.pw_1) < MINIMUM_PASSWORD_LENGTH:
await self.animated_text.display_text(False, f"Passwort muss mindestens {MINIMUM_PASSWORD_LENGTH} Zeichen lang sein!") await self.display_animated_text(False, f"Passwort muss mindestens {MINIMUM_PASSWORD_LENGTH} Zeichen lang sein!")
self.submit_button.is_loading = False self.submit_button_loading = False
return return
if not self.email_input.is_valid or len(self.email_input.text) < 3: if not self.email_valid or len(self.email) < 3:
await self.animated_text.display_text(False, "E-Mail Adresse ungültig!") await self.display_animated_text(False, "E-Mail Adresse ungültig!")
self.submit_button.is_loading = False self.submit_button_loading = False
return return
user_service = self.session[UserService] user_service = self.session[UserService]
mailing_service = self.session[MailingService] mailing_service = self.session[MailingService]
lan_info = self.session[ConfigurationService].get_lan_info() lan_info = self.session[ConfigurationService].get_lan_info()
if await user_service.get_user(self.email_input.text) is not None or await user_service.get_user(self.user_name_input.text) is not None: 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.animated_text.display_text(False, "Benutzername oder E-Mail bereits regestriert!") await self.display_animated_text(False, "Benutzername oder E-Mail bereits registriert!")
self.submit_button.is_loading = False self.submit_button_loading = False
return return
try: try:
new_user = await user_service.create_user(self.user_name_input.text, self.email_input.text, self.pw_1.text) new_user = await user_service.create_user(self.user_name, self.email, self.pw_1)
if not new_user: if not new_user:
logger.warning(f"UserService.create_user returned: {new_user}") # ToDo: Seems like the user is created fine, even if not returned #FixMe logger.error(f"create_user returned: {new_user}")
raise Exception(f"create_user returned: {new_user}")
except Exception as e: except Exception as e:
logger.error(f"Unknown error during new user registration: {e}") logger.error(f"Unknown error during new user registration: {e}")
await self.animated_text.display_text(False, "Es ist ein unbekannter Fehler aufgetreten :(") await self.display_animated_text(False, "Es ist ein unbekannter Fehler aufgetreten :(")
self.submit_button.is_loading = False self.submit_button_loading = False
return return
await mailing_service.send_email( await mailing_service.send_email(
subject="Erfolgreiche Registrierung", subject="Erfolgreiche Registrierung",
body=f"Hallo {self.user_name_input.text},\n\n" body=f"Hallo {self.user_name},\n\n"
f"Du hast dich erfolgreich beim EZGG-LAN Manager für {lan_info.name} {lan_info.iteration} registriert.\n\n" f"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"Wenn du dich nicht registriert hast, kontaktiere bitte unser Team über unsere Homepage.\n\n"
f"Liebe Grüße\nDein {lan_info.name} - Team", f"Liebe Grüße\nDein {lan_info.name} - Team",
receiver=self.email_input.text receiver=self.email
) )
self.submit_button.is_loading = False self.submit_button_loading = False
await self.animated_text.display_text(True, "Erfolgreich registriert!") await self.display_animated_text(True, "Erfolgreich registriert!")
@event.on_populate @event.on_populate
async def on_populate(self) -> None: async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Registrieren") 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: def build(self) -> Component:
self.user_name_input = TextInput( user_name_input = TextInput(
label="Benutzername", label="Benutzername",
text="", text=self.bind().user_name,
margin_left=1, margin_left=1,
margin_right=1, margin_right=1,
margin_bottom=1, margin_bottom=1,
grow_x=True, grow_x=True,
on_lose_focus=self.on_user_name_focus_loss on_lose_focus=self.on_user_name_focus_loss
) )
self.email_input = TextInput( email_input = TextInput(
label="E-Mail Adresse", label="E-Mail Adresse",
text="", text=self.bind().email,
margin_left=1, margin_left=1,
margin_right=1, margin_right=1,
margin_bottom=1, margin_bottom=1,
grow_x=True, grow_x=True,
on_lose_focus=self.on_email_focus_loss on_lose_focus=self.on_email_focus_loss,
is_valid=self.email_valid
) )
self.pw_1 = TextInput( pw_1_input = TextInput(
label="Passwort", label="Passwort",
text="", text=self.bind().pw_1,
margin_left=1, margin_left=1,
margin_right=1, margin_right=1,
margin_bottom=1, margin_bottom=1,
grow_x=True, grow_x=True,
is_secret=True, is_secret=True,
on_lose_focus=self.on_pw_focus_loss on_lose_focus=self.on_pw_focus_loss,
is_valid=self.pw_1_valid
) )
self.pw_2 = TextInput( pw_2_input = TextInput(
label="Passwort wiederholen", label="Passwort wiederholen",
text="", text=self.bind().pw_2,
margin_left=1, margin_left=1,
margin_right=1, margin_right=1,
margin_bottom=1, margin_bottom=1,
grow_x=True, grow_x=True,
is_secret=True, is_secret=True,
on_lose_focus=self.on_pw_focus_loss on_lose_focus=self.on_pw_focus_loss,
is_valid=self.pw_2_valid
) )
self.submit_button = Button( submit_button = Button(
content=Text( content=Text(
"Registrieren", "Registrieren",
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9), style=TextStyle(fill=self.session.theme.background_color, font_size=0.9),
@@ -145,13 +174,8 @@ class RegisterPage(Component):
shape="rectangle", shape="rectangle",
style="minor", style="minor",
color=self.session.theme.secondary_color, color=self.session.theme.secondary_color,
on_press=self.on_submit_button_pressed on_press=self.on_submit_button_pressed,
) is_loading=self.submit_button_loading
self.animated_text = AnimatedText(
margin_top=2,
margin_left=1,
margin_right=1,
margin_bottom=2
) )
return Column( return Column(
MainViewContentBox( MainViewContentBox(
@@ -166,12 +190,12 @@ class RegisterPage(Component):
margin_bottom=2, margin_bottom=2,
align_x=0.5 align_x=0.5
), ),
self.user_name_input, user_name_input,
self.email_input, email_input,
self.pw_1, pw_1_input,
self.pw_2, pw_2_input,
self.submit_button, submit_button,
self.animated_text Text(self.display_text, margin_top=2, margin_left=1, margin_right=1, margin_bottom=2, style=self.display_text_style)
) )
), ),
align_y=0, 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.components.SeatingPurchaseBox import SeatingPurchaseBox
from src.ezgg_lan_manager.services.SeatingService import NoTicketError, SeatNotFoundError, WrongCategoryError, SeatAlreadyTakenError 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.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.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
logger = logging.getLogger(__name__.split(".")[-1]) logger = logging.getLogger(__name__.split(".")[-1])
@@ -37,7 +37,10 @@ class SeatingPlanPage(Component):
async def on_populate(self) -> None: async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Sitzplan") await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Sitzplan")
self.seating_info = await self.session[SeatingService].get_seating() self.seating_info = await self.session[SeatingService].get_seating()
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id) try:
self.user = await self.session[UserService].get_user(self.session[UserSession].user_id)
except KeyError:
self.user = None
if not self.user: if not self.user:
self.is_booking_blocked = True self.is_booking_blocked = True
else: else:
+176
View File
@@ -0,0 +1,176 @@
from asyncio import sleep
from rio import event, ProgressCircle, PointerEventListener, PointerEvent, Popup, Color
from src.ezgg_lan_manager import ConfigurationService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.TeamRevealer import TeamRevealer
from src.ezgg_lan_manager.components.TeamsDialogHandler import *
from src.ezgg_lan_manager.services.RefreshService import RefreshService
from src.ezgg_lan_manager.services.TeamService import TeamService
from src.ezgg_lan_manager.services.UserService import UserService
from src.ezgg_lan_manager.types.Team import Team
from src.ezgg_lan_manager.types.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
class TeamsPage(Component):
all_teams: Optional[list[Team]] = None
user: Optional[User] = None
# Dialog handling
popup_open: bool = False
join_active: bool = False
leave_active: bool = True
create_active: bool = False
selected_team_for_join_or_leave: Optional[Team] = None
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Teams")
self.session[RefreshService].subscribe(self.on_populate)
self.all_teams = await self.session[TeamService].get_all_teams()
try:
self.user = await self.session[UserService].get_user(self.session[UserSession].user_id)
except KeyError:
self.user = None
async def on_join_button_pressed(self, team: Team) -> None:
if self.user is None:
return
self.selected_team_for_join_or_leave = team
self.join_active, self.leave_active, self.create_active = True, False, False
self.popup_open = True
async def on_leave_button_pressed(self, team: Team) -> None:
if self.user is None:
return
self.selected_team_for_join_or_leave = team
self.join_active, self.leave_active, self.create_active = False, True, False
self.popup_open = True
async def on_create_button_pressed(self, _: PointerEvent) -> None:
if self.user is None:
return
self.join_active, self.leave_active, self.create_active = False, False, True
self.popup_open = True
async def popup_action_cancelled(self) -> None:
self.popup_open = False
await sleep(0.2) # Waits for the animation to play before resetting its contents
self.join_active, self.leave_active, self.create_active = False, False, False
self.selected_team_for_join_or_leave = None
self.all_teams = await self.session[TeamService].get_all_teams()
def build(self) -> Component:
if self.all_teams is None:
return Column(
MainViewContentBox(
ProgressCircle(
color="secondary",
align_x=0.5,
margin_top=1,
margin_bottom=1
)
),
Spacer()
)
team_list = []
for team in self.all_teams:
team_list.append(
TeamRevealer(
user=self.user,
team=team,
mode="leave" if self.user in team.members.keys() else "join",
on_button_pressed=self.on_leave_button_pressed if self.user in team.members.keys() else self.on_join_button_pressed
)
)
if team_list:
team_list[-1].margin_bottom = 1
own_teams_content = Spacer(grow_x=False, grow_y=False)
if self.user is not None:
user_team_list = []
for team in self.all_teams:
if self.user in team.members.keys():
user_team_list.append(TeamRevealer(user=self.user, team=team, mode="leave", on_button_pressed=self.on_leave_button_pressed))
if not user_team_list:
user_team_list.append(Text(
text="Du bist noch in keinem Team.",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1
),
margin_top=1,
margin_bottom=1,
align_x=0.5
))
else:
user_team_list[-1].margin_bottom = 1
own_teams_content = MainViewContentBox(
Column(
Row(
Text(
text="Deine Teams",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
grow_x=True,
justify="right",
margin_right=3
),
Column(
PointerEventListener(Rectangle(
content=Text(text="Team erstellen", style=TextStyle(fill=self.session.theme.background_color, font_size=0.7), margin=0.1, selectable=False),
stroke_width=0.1,
stroke_color=self.session.theme.hud_color,
cursor="pointer",
hover_fill=self.session.theme.hud_color,
transition_time=0
), on_press=self.on_create_button_pressed),
Spacer(),
margin_right=2
),
margin_top=1,
margin_bottom=1
),
*user_team_list
)
)
return Popup(
anchor=Column(
own_teams_content,
MainViewContentBox(
Column(
Text(
text="Alle Teams",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=1,
margin_bottom=1,
align_x=0.5
),
*team_list
)
),
align_y=0,
),
content=Column(
TeamsDialogJoinHandler(is_active=self.join_active, cancel=self.popup_action_cancelled, user=self.user, team=self.selected_team_for_join_or_leave),
TeamsDialogLeaveHandler(is_active=self.leave_active, cancel=self.popup_action_cancelled, user=self.user, team=self.selected_team_for_join_or_leave),
TeamsDialogCreateHandler(is_active=self.create_active, cancel=self.popup_action_cancelled, user=self.user)
),
is_open=self.popup_open,
modal=False,
corner_radius=(0.5, 0.5, 0.5, 0.5),
color=Color.TRANSPARENT,
user_closable=False,
position="top"
)
@@ -1,27 +1,36 @@
import logging
from asyncio import sleep
from functools import partial
from typing import Optional, Union, Literal from typing import Optional, Union, Literal
from from_root import from_root from from_root import from_root
from rio import Column, Component, event, TextStyle, Text, Row, Image, Spacer, ProgressCircle, Button, Checkbox, ThemeContextSwitcher, Link, Revealer, PointerEventListener, \ from rio import Column, Component, event, TextStyle, Text, Row, Image, Spacer, ProgressCircle, Button, Checkbox, ThemeContextSwitcher, Link, Revealer, PointerEventListener, \
PointerEvent, Rectangle, Color PointerEvent, Rectangle, Color, Popup, Dropdown
from src.ezgg_lan_manager import ConfigurationService, TournamentService, UserService from src.ezgg_lan_manager import ConfigurationService, TournamentService, UserService, TicketingService, TeamService, RefreshService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.TournamentDetailsInfoRow import TournamentDetailsInfoRow 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.DateUtil import weekday_to_display_text
from src.ezgg_lan_manager.types.Participant import Participant from src.ezgg_lan_manager.types.Team import Team, TeamStatus
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
from src.ezgg_lan_manager.types.Tournament import Tournament from src.ezgg_lan_manager.types.Tournament import Tournament
from src.ezgg_lan_manager.types.TournamentBase import TournamentStatus, tournament_status_to_display_text, tournament_format_to_display_texts from src.ezgg_lan_manager.types.TournamentBase import TournamentStatus, tournament_status_to_display_text, tournament_format_to_display_texts, ParticipantType, TournamentFormat
from src.ezgg_lan_manager.types.User import User 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): class TournamentDetailsPage(Component):
tournament: Optional[Union[Tournament, str]] = None tournament: Optional[Union[Tournament, str]] = None
rules_accepted: bool = False rules_accepted: bool = False
user: Optional[User] = None user: Optional[User] = None
user_teams: list[Team] = []
loading: bool = False loading: bool = False
participant_revealer_open: bool = False participant_revealer_open: bool = False
current_tournament_user_list: list[User] = [] # ToDo: Integrate Teams current_tournament_user_or_team_list: Union[list[User], list[Team]] = []
team_dialog_open: bool = False
team_register_options: dict[str, Optional[Team]] = {"": None}
team_selected_for_register: Optional[Team] = None
# State for message above register button # State for message above register button
message: str = "" message: str = ""
@@ -35,16 +44,41 @@ class TournamentDetailsPage(Component):
tournament_id = None tournament_id = None
if tournament_id is not None: if tournament_id is not None:
self.tournament = await self.session[TournamentService].get_tournament_by_id(tournament_id) self.tournament = await self.session[TournamentService].get_tournament_by_id(tournament_id)
if self.tournament is not None: if isinstance(self.tournament, Tournament):
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - {self.tournament.name}") await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - {self.tournament.name}")
self.current_tournament_user_list = await self.session[TournamentService].get_users_from_participant_list(self.tournament.participants) if self.tournament.participant_type == ParticipantType.PLAYER:
self.current_tournament_user_or_team_list = await self.session[TournamentService].get_users_from_participant_list(self.tournament.participants)
elif self.tournament.participant_type == ParticipantType.TEAM:
self.current_tournament_user_or_team_list = await self.session[TournamentService].get_teams_from_participant_list(self.tournament.participants)
else: else:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Turniere") await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Turniere")
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id) try:
user_id = self.session[UserSession].user_id
self.user = await self.session[UserService].get_user(user_id)
self.user_teams = await self.session[TeamService].get_teams_for_user_by_id(user_id)
except KeyError:
self.user = None
self.user_teams = []
self.session[RefreshService].subscribe(self.on_populate)
self.loading_done() 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: def open_close_participant_revealer(self, _: PointerEvent) -> None:
self.participant_revealer_open = not self.participant_revealer_open self.participant_revealer_open = not self.participant_revealer_open
@@ -53,38 +87,100 @@ class TournamentDetailsPage(Component):
if not self.user: if not self.user:
return return
user_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id)
if user_ticket is None:
self.is_success = False
self.message = "Turnieranmeldung nur mit Ticket"
elif not isinstance(self.tournament, Tournament):
self.is_success = False
self.message = "Fehler bei der Anmeldung"
else:
# Register single player
if self.tournament.participant_type == ParticipantType.PLAYER:
try: try:
await self.session[TournamentService].register_user_for_tournament(self.user.user_id, self.tournament.id) await self.session[TournamentService].register_user_for_tournament(self.user.user_id, self.tournament.id)
await self.artificial_delay()
self.is_success = True self.is_success = True
self.message = f"Erfolgreich angemeldet!" self.message = f"Erfolgreich angemeldet!"
except Exception as e: except Exception as e:
logger.error(e)
self.is_success = False self.is_success = False
self.message = f"Fehler: {e}" 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 self.loading = False
await self.on_populate()
async def unregister_pressed(self) -> None: async def on_team_register_confirmed(self) -> None:
if self.team_selected_for_register is None:
await self.on_team_register_canceled()
return
try:
if isinstance(self.tournament, Tournament):
await self.session[TournamentService].register_team_for_tournament(self.team_selected_for_register.id, self.tournament.id)
await self.artificial_delay()
self.is_success = True
self.message = f"Erfolgreich angemeldet!"
self.team_dialog_open = False
self.team_selected_for_register = None
else:
raise ValueError("Turnier nicht gefunden")
except Exception as e:
logger.error(e)
self.message = f"Fehler: {e}"
self.is_success = False
await self.update()
self.loading = False
async def on_team_register_canceled(self) -> None:
self.team_dialog_open = False
self.team_selected_for_register = None
self.loading = False
async def unregister_pressed(self, team: Optional[Team] = None) -> None:
self.loading = True self.loading = True
if not self.user: if not self.user:
return return
try: 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) await self.session[TournamentService].unregister_user_from_tournament(self.user.user_id, self.tournament.id)
elif isinstance(self.tournament, Tournament) and self.tournament.participant_type == ParticipantType.TEAM:
if team.members[self.user] == TeamStatus.OFFICER or team.members[self.user] == TeamStatus.LEADER:
await self.session[TournamentService].unregister_team_from_tournament(team.id, self.tournament.id)
else:
raise PermissionError("Nur Leiter und Offiziere können das Team abmelden")
await self.artificial_delay()
self.is_success = True self.is_success = True
self.message = f"Erfolgreich abgemeldet!" self.message = f"Erfolgreich abgemeldet!"
except Exception as e: except Exception as e:
self.is_success = False self.is_success = False
self.message = f"Fehler: {e}" self.message = f"Fehler: {e}"
await self.update()
self.loading = False self.loading = False
await self.on_populate()
async def tree_button_clicked(self) -> None: async def tree_button_clicked(self) -> None:
pass # ToDo: Implement tournament tree view if isinstance(self.tournament, Tournament):
self.session.navigate_to(f"tournament-tree?id={self.tournament.id}")
def loading_done(self) -> None: def loading_done(self) -> None:
if self.tournament is None: if self.tournament is None:
self.tournament = "Turnier konnte nicht gefunden werden" 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: def build(self) -> Component:
if self.tournament is None: if self.tournament is None:
@@ -119,6 +215,7 @@ class TournamentDetailsPage(Component):
tournament_status_color = self.session.theme.danger_color tournament_status_color = self.session.theme.danger_color
elif self.tournament.status == TournamentStatus.ONGOING or self.tournament.status == TournamentStatus.COMPLETED: elif self.tournament.status == TournamentStatus.ONGOING or self.tournament.status == TournamentStatus.COMPLETED:
tournament_status_color = self.session.theme.warning_color tournament_status_color = self.session.theme.warning_color
if self.tournament.format != TournamentFormat.FFA:
tree_button = Button( tree_button = Button(
content="Turnierbaum anzeigen", content="Turnierbaum anzeigen",
shape="rectangle", shape="rectangle",
@@ -130,10 +227,12 @@ class TournamentDetailsPage(Component):
on_press=self.tree_button_clicked on_press=self.tree_button_clicked
) )
# ToDo: Integrate Teams logic
ids_of_participants = [p.id for p in self.tournament.participants] ids_of_participants = [p.id for p in self.tournament.participants]
color_key: Literal["hud", "danger"] = "hud" color_key: Literal["hud", "danger"] = "hud"
on_press_function = self.register_pressed 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 if self.user and self.user.user_id in ids_of_participants: # User already registered for tournament
button_text = "Abmelden" button_text = "Abmelden"
button_sensitive_hook = True # User has already accepted the rules previously button_sensitive_hook = True # User has already accepted the rules previously
@@ -146,6 +245,29 @@ class TournamentDetailsPage(Component):
# This should NEVER happen # This should NEVER happen
button_text = "Anmelden" button_text = "Anmelden"
button_sensitive_hook = False button_sensitive_hook = False
elif self.tournament.participant_type == ParticipantType.TEAM:
self.current_tournament_user_or_team_list: list[Team] # IDE TypeHint
participant_names = "\n".join([t.name for t in self.current_tournament_user_or_team_list])
user_team_registered = []
for team in self.user_teams:
if team.id in ids_of_participants:
user_team_registered.append(team)
if self.user and len(user_team_registered) > 0: # Any of the users teams already registered for tournament
button_text = f"{user_team_registered[0].abbreviation} abmelden"
button_sensitive_hook = True # User has already accepted the rules previously
color_key = "danger"
on_press_function = partial(self.unregister_pressed, user_team_registered[0])
elif self.user and len(user_team_registered) == 0:
button_text = "Anmelden"
button_sensitive_hook = self.rules_accepted
else:
# This should NEVER happen
button_text = "Anmelden"
button_sensitive_hook = False
else:
logger.fatal("Did someone add new values to ParticipantType ? ;)")
return Column()
if self.tournament.status != TournamentStatus.OPEN or self.tournament.is_full: 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 button_sensitive_hook = False # Override button controls if tournament is not open anymore or full
@@ -171,8 +293,6 @@ class TournamentDetailsPage(Component):
# No UI here if user not logged in # No UI here if user not logged in
accept_rules_row, button = Spacer(), Spacer() accept_rules_row, button = Spacer(), Spacer()
content = Column( content = Column(
Row( Row(
Image(image=from_root(f"src/ezgg_lan_manager/assets/img/games/{self.tournament.game_title.image_name}"), margin_right=1), Image(image=from_root(f"src/ezgg_lan_manager/assets/img/games/{self.tournament.game_title.image_name}"), margin_right=1),
@@ -198,7 +318,7 @@ class TournamentDetailsPage(Component):
content=Rectangle( content=Rectangle(
content=TournamentDetailsInfoRow( content=TournamentDetailsInfoRow(
"Teilnehmer ▴" if self.participant_revealer_open else "Teilnehmer ▾", "Teilnehmer ▴" if self.participant_revealer_open else "Teilnehmer ▾",
f"{len(self.tournament.participants)} / {self.tournament.max_participants}", f"{len(self.current_tournament_user_or_team_list)} / {self.tournament.max_participants}",
value_color=self.session.theme.danger_color if self.tournament.is_full else self.session.theme.background_color, 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 key_color=self.session.theme.secondary_color
), ),
@@ -210,7 +330,7 @@ class TournamentDetailsPage(Component):
Revealer( Revealer(
header=None, header=None,
content=Text( content=Text(
"\n".join([u.user_name for u in self.current_tournament_user_list]), # ToDo: Integrate Teams participant_names,
style=TextStyle(fill=self.session.theme.background_color) style=TextStyle(fill=self.session.theme.background_color)
), ),
is_open=self.participant_revealer_open, is_open=self.participant_revealer_open,
@@ -240,6 +360,39 @@ class TournamentDetailsPage(Component):
button 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( return Column(
MainViewContentBox( MainViewContentBox(
Column( Column(
@@ -0,0 +1,287 @@
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 calculate_font_size(self, txt: str) -> float:
if len(txt) < 14:
return 0.9
elif len(txt) < 20:
return 0.7
return 0.6
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=self.calculate_font_size(self.opponent_1)
),
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=self.calculate_font_size(self.opponent_1)
)
),
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=self.calculate_font_size(self.opponent_2)
),
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=self.calculate_font_size(self.opponent_2)
)
),
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
)
+5
View File
@@ -22,3 +22,8 @@ from .ManageTournamentsPage import ManageTournamentsPage
from .OverviewPage import OverviewPage from .OverviewPage import OverviewPage
from .TournamentDetailsPage import TournamentDetailsPage from .TournamentDetailsPage import TournamentDetailsPage
from .TournamentRulesPage import TournamentRulesPage from .TournamentRulesPage import TournamentRulesPage
from .ConwayPage import ConwayPage
from .TeamsPage import TeamsPage
from .AdminNavigationPage import AdminNavigationPage
from .TournamentTreePage import TournamentTreePage
from .NewPosOrderPage import NewPosOrderPage
@@ -1,4 +1,6 @@
import io
import logging import logging
import qrcode
from collections.abc import Callable from collections.abc import Callable
from datetime import datetime from datetime import datetime
from decimal import Decimal, ROUND_DOWN from decimal import Decimal, ROUND_DOWN
@@ -74,3 +76,29 @@ class AccountingService:
return "0.00 €" return "0.00 €"
rounded_decimal = str(euros.quantize(Decimal(".01"), rounding=ROUND_DOWN)) rounded_decimal = str(euros.quantize(Decimal(".01"), rounding=ROUND_DOWN))
return f"{rounded_decimal}" 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()
@@ -36,7 +36,7 @@ class CateringService:
# ORDERS # ORDERS
async def place_order(self, menu_items: CateringMenuItemsWithAmount, user_id: int, async def place_order(self, menu_items: CateringMenuItemsWithAmount, user_id: int,
is_delivery: bool = True) -> CateringOrder: is_delivery: bool = True, print_receipt: bool = True) -> CateringOrder:
for menu_item in menu_items: for menu_item in menu_items:
if menu_item.is_disabled: if menu_item.is_disabled:
raise CateringError("Order includes disabled items", CateringErrorType.INCLUDES_DISABLED_ITEM) raise CateringError("Order includes disabled items", CateringErrorType.INCLUDES_DISABLED_ITEM)
@@ -54,6 +54,7 @@ class CateringService:
await self._accounting_service.remove_balance(user_id, total_price, f"CATERING - {order.order_id}") await self._accounting_service.remove_balance(user_id, total_price, f"CATERING - {order.order_id}")
logger.info( logger.info(
f"User '{order.customer.user_name}' (ID:{order.customer.user_id}) ordered from catering for {self._accounting_service.make_euro_string_from_decimal(total_price)}") f"User '{order.customer.user_name}' (ID:{order.customer.user_id}) ordered from catering for {self._accounting_service.make_euro_string_from_decimal(total_price)}")
if print_receipt:
await self._receipt_printing_service.print_order(user, order) await self._receipt_printing_service.print_order(user, order)
# await self.cancel_order(order) # ToDo: Check if commented out before commit. Un-comment to auto-cancel every placed order # await self.cancel_order(order) # ToDo: Check if commented out before commit. Un-comment to auto-cancel every placed order
return order return order
+255 -22
View File
@@ -1,7 +1,6 @@
import logging import logging
from datetime import date, datetime from datetime import date, datetime, UTC
from pprint import pprint
from typing import Optional from typing import Optional
from decimal import Decimal from decimal import Decimal
@@ -14,9 +13,10 @@ from src.ezgg_lan_manager.types.ConfigurationTypes import DatabaseConfiguration
from src.ezgg_lan_manager.types.News import News from src.ezgg_lan_manager.types.News import News
from src.ezgg_lan_manager.types.Participant import Participant from src.ezgg_lan_manager.types.Participant import Participant
from src.ezgg_lan_manager.types.Seat import Seat 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.Ticket import Ticket
from src.ezgg_lan_manager.types.Tournament import Tournament from src.ezgg_lan_manager.types.Tournament import Tournament
from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, ParticipantType from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, ParticipantType, MatchStatus
from src.ezgg_lan_manager.types.Transaction import Transaction from src.ezgg_lan_manager.types.Transaction import Transaction
from src.ezgg_lan_manager.types.User import User from src.ezgg_lan_manager.types.User import User
@@ -62,7 +62,8 @@ class DatabaseService:
password=self._database_config.db_password, password=self._database_config.db_password,
db=self._database_config.db_name, db=self._database_config.db_name,
minsize=1, minsize=1,
maxsize=40 maxsize=40,
autocommit=True
) )
except aiomysql.OperationalError: except aiomysql.OperationalError:
return False return False
@@ -75,14 +76,15 @@ class DatabaseService:
user_name=data[1], user_name=data[1],
user_mail=data[2], user_mail=data[2],
user_password=data[3], user_password=data[3],
user_first_name=data[4], user_fallback_password=data[4],
user_last_name=data[5], user_first_name=data[5],
user_birth_day=data[6], user_last_name=data[6],
is_active=bool(data[7]), user_birth_day=data[7],
is_team_member=bool(data[8]), is_active=bool(data[8]),
is_admin=bool(data[9]), is_team_member=bool(data[9]),
created_at=data[10], is_admin=bool(data[10]),
last_updated_at=data[11] created_at=data[11],
last_updated_at=data[12]
) )
@staticmethod @staticmethod
@@ -185,10 +187,10 @@ class DatabaseService:
async with conn.cursor(aiomysql.Cursor) as cursor: async with conn.cursor(aiomysql.Cursor) as cursor:
try: try:
await cursor.execute( await cursor.execute(
"UPDATE users SET user_name=%s, user_mail=%s, user_password=%s, user_first_name=%s, " "UPDATE users SET user_name=%s, user_mail=%s, user_password=%s, user_fallback_password=%s,"
"user_last_name=%s, user_birth_date=%s, is_active=%s, is_team_member=%s, is_admin=%s " "user_first_name=%s, user_last_name=%s, user_birth_date=%s, is_active=%s, is_team_member=%s,"
"WHERE (user_id=%s)", " is_admin=%s WHERE (user_id=%s)",
(user.user_name, user.user_mail.lower(), user.user_password, (user.user_name, user.user_mail.lower(), user.user_password, user.user_fallback_password,
user.user_first_name, user.user_last_name, user.user_birth_day, user.user_first_name, user.user_last_name, user.user_birth_day,
user.is_active, user.is_team_member, user.is_admin, user.is_active, user.is_team_member, user.is_admin,
user.user_id) user.user_id)
@@ -447,7 +449,7 @@ class DatabaseService:
pool_init_result = await self.init_db_pool() pool_init_result = await self.init_db_pool()
if not pool_init_result: if not pool_init_result:
raise NoDatabaseConnectionError raise NoDatabaseConnectionError
return await self.change_ticket_owner(ticket_id) return await self.delete_ticket(ticket_id)
except Exception as e: except Exception as e:
logger.warning(f"Error deleting ticket: {e}") logger.warning(f"Error deleting ticket: {e}")
return False return False
@@ -859,6 +861,7 @@ class DatabaseService:
t.status AS tournament_status, t.status AS tournament_status,
t.max_participants, t.max_participants,
t.created_at, t.created_at,
t.participant_type AS tournament_participant_type,
/* ======================= /* =======================
Game Title Game Title
@@ -874,6 +877,7 @@ class DatabaseService:
======================= */ ======================= */
tp.id AS participant_id, tp.id AS participant_id,
tp.user_id, tp.user_id,
tp.team_id,
tp.participant_type, tp.participant_type,
tp.seed, tp.seed,
tp.joined_at tp.joined_at
@@ -907,6 +911,8 @@ class DatabaseService:
if current_tournament is None or current_tournament.id != row["tournament_id"]: if current_tournament is None or current_tournament.id != row["tournament_id"]:
if current_tournament is not None: if current_tournament is not None:
tournaments.append(current_tournament) 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( current_tournament = Tournament(
id_=row["tournament_id"], id_=row["tournament_id"],
name=row["tournament_name"], name=row["tournament_name"],
@@ -920,14 +926,16 @@ class DatabaseService:
format_=self._parse_tournament_format(row["tournament_format"]), format_=self._parse_tournament_format(row["tournament_format"]),
start_time=row["start_time"], start_time=row["start_time"],
status=self._parse_tournament_status(row["tournament_status"]), status=self._parse_tournament_status(row["tournament_status"]),
participants=[Participant(id_=row["user_id"], participant_type=self._parse_participant_type(row["participant_type"]))] if row["user_id"] is not None else [], participants=[Participant(id_=row[id_accessor], participant_type=self._parse_participant_type(row["participant_type"]))] if row[id_accessor] is not None else [],
matches=None, # ToDo: Implement matches=None, # ToDo: Implement
rounds=[], # ToDo: Implement rounds=[], # ToDo: Implement
max_participants=row["max_participants"] max_participants=row["max_participants"],
participant_type=participant_type
) )
else: else:
id_accessor = "user_id" if current_tournament.participant_type == ParticipantType.PLAYER else "team_id"
current_tournament.add_participant( current_tournament.add_participant(
Participant(id_=row["user_id"], participant_type=self._parse_participant_type(row["participant_type"])) Participant(id_=row[id_accessor], participant_type=self._parse_participant_type(row["participant_type"]))
) )
else: else:
tournaments.append(current_tournament) tournaments.append(current_tournament)
@@ -935,11 +943,14 @@ class DatabaseService:
return tournaments return tournaments
async def add_participant_to_tournament(self, participant: Participant, tournament: Tournament) -> None: 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 self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor: async with conn.cursor(aiomysql.Cursor) as cursor:
try: try:
await cursor.execute( await cursor.execute(
"INSERT INTO tournament_participants (tournament_id, user_id, participant_type) VALUES (%s, %s, %s);", f"INSERT INTO tournament_participants (tournament_id, {accessor}, participant_type) VALUES (%s, %s, %s);",
(tournament.id, participant.id, participant.participant_type.name) (tournament.id, participant.id, participant.participant_type.name)
) )
await conn.commit() await conn.commit()
@@ -952,11 +963,12 @@ class DatabaseService:
logger.warning(f"Error adding participant to tournament: {e}") logger.warning(f"Error adding participant to tournament: {e}")
async def remove_participant_from_tournament(self, participant: Participant, tournament: Tournament) -> None: 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 self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor: async with conn.cursor(aiomysql.Cursor) as cursor:
try: try:
await cursor.execute( await cursor.execute(
"DELETE FROM tournament_participants WHERE (tournament_id = %s AND user_id = %s);", f"DELETE FROM tournament_participants WHERE (tournament_id = %s AND {accessor} = %s);",
(tournament.id, participant.id) (tournament.id, participant.id)
) )
await conn.commit() await conn.commit()
@@ -967,3 +979,224 @@ class DatabaseService:
return await self.remove_participant_from_tournament(participant, tournament) return await self.remove_participant_from_tournament(participant, tournament)
except Exception as e: except Exception as e:
logger.warning(f"Error removing participant from tournament: {e}") logger.warning(f"Error removing participant from tournament: {e}")
async def get_teams(self) -> list[Team]:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
query = """
SELECT
t.id AS team_id,
t.name AS team_name,
t.abbreviation AS team_abbr,
t.join_password,
t.created_at AS team_created_at,
tm.status AS team_status,
tm.joined_at AS member_joined_at,
u.*
FROM teams t
LEFT JOIN team_members tm
ON t.id = tm.team_id
LEFT JOIN users u
ON tm.user_id = u.user_id
ORDER BY
t.id,
CASE tm.status
WHEN 'LEADER' THEN 1
WHEN 'OFFICER' THEN 2
WHEN 'MEMBER' THEN 3
ELSE 4
END,
u.user_name;
"""
try:
await cursor.execute(query)
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.get_teams()
except Exception as e:
logger.warning(f"Error getting teams: {e}")
return []
current_team: Optional[Team] = None
all_teams = []
for row in await cursor.fetchall():
if row[5] is None: # Teams without single member are ignored
continue
if current_team is None:
user = self._map_db_result_to_user(row[7:])
current_team = Team(id=row[0], name=row[1], abbreviation=row[2], join_password=row[3], members={user: TeamStatus.from_str(row[5])})
elif current_team.id == row[0]: # Still same team
current_team.members[self._map_db_result_to_user(row[7:])] = TeamStatus.from_str(row[5])
else:
all_teams.append(current_team)
user = self._map_db_result_to_user(row[7:])
current_team = Team(id=row[0], name=row[1], abbreviation=row[2], join_password=row[3], members={user: TeamStatus.from_str(row[5])})
if current_team is not None:
all_teams.append(current_team)
return all_teams
async def get_team_by_id(self, team_id: int) -> Optional[Team]:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
query = """
SELECT
t.id AS team_id,
t.name AS team_name,
t.abbreviation AS team_abbr,
t.join_password,
t.created_at AS team_created_at,
tm.status AS team_status,
tm.joined_at AS member_joined_at,
u.*
FROM teams t
LEFT JOIN team_members tm
ON t.id = tm.team_id
LEFT JOIN users u
ON tm.user_id = u.user_id
WHERE t.id = %s
ORDER BY
t.id,
CASE tm.status
WHEN 'LEADER' THEN 1
WHEN 'OFFICER' THEN 2
WHEN 'MEMBER' THEN 3
ELSE 4
END,
u.user_name;
"""
try:
await cursor.execute(query, (team_id, ))
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.get_team_by_id(team_id)
except Exception as e:
logger.warning(f"Error getting team: {e}")
return None
team: Optional[Team] = None
for row in await cursor.fetchall():
if team is None:
user = self._map_db_result_to_user(row[7:])
team = Team(id=row[0], name=row[1], abbreviation=row[2], join_password=row[3], members={user: TeamStatus.from_str(row[5])})
elif team.id == row[0]:
team.members[self._map_db_result_to_user(row[7:])] = TeamStatus.from_str(row[5])
return team
async def create_team(self, team_name: str, team_abbr: str, join_password: str, leader: User) -> Team:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"INSERT INTO teams (name, abbreviation, join_password) "
"VALUES (%s, %s, %s)", (team_name, team_abbr, join_password)
)
await conn.commit()
team_id = cursor.lastrowid
await cursor.execute(
"INSERT INTO team_members (team_id, user_id, status) VALUES (%s, %s, %s)",
(team_id, leader.user_id, TeamStatus.LEADER.name)
)
await conn.commit()
return await self.get_team_by_id(team_id)
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.create_team(team_name, team_abbr, join_password)
except aiomysql.IntegrityError as e:
logger.warning(f"Aborted duplication entry: {e}")
raise DuplicationError
async def update_team(self, team: Team) -> Team:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"UPDATE teams SET name = %s, abbreviation = %s, join_password = %s WHERE (id = %s)",
(team.name, team.abbreviation, team.join_password, team.id)
)
await conn.commit()
return await self.get_team_by_id(team.id)
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.update_team(team)
except aiomysql.IntegrityError as e:
logger.warning(f"Aborted duplication entry: {e}")
raise DuplicationError
async def add_member_to_team(self, team: Team, user: User, status: TeamStatus = TeamStatus.MEMBER) -> None:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"INSERT INTO team_members (team_id, user_id, status) VALUES (%s, %s, %s)",
(team.id, user.user_id, status.name)
)
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.add_member_to_team(team, user, status)
except aiomysql.IntegrityError as e:
logger.warning(f"Failed to add member {user.user_name} to team {team.name}: {e}")
raise DuplicationError
async def remove_user_from_team(self, team: Team, user: User) -> None:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"DELETE FROM team_members WHERE team_id = %s AND user_id = %s",
(team.id, user.user_id)
)
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.remove_user_from_team(team, user)
async def change_tournament_status(self, tournament_id: int, status: TournamentStatus) -> None:
async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor:
try:
await cursor.execute(
"UPDATE tournaments SET status = %s WHERE (id = %s)",
(status.name, tournament_id)
)
await conn.commit()
except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool()
if not pool_init_result:
raise NoDatabaseConnectionError
return await self.change_tournament_status(tournament_id, status)
@@ -3,23 +3,24 @@ from typing import Optional
from rio import UserSettings from rio import UserSettings
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage from src.ezgg_lan_manager.types.UserSession import UserSession
class LocalData(UserSettings): class LocalData(UserSettings):
stored_session_token: Optional[str] = None stored_session_token: Optional[str]
class LocalDataService: class LocalDataService:
def __init__(self) -> None: def __init__(self) -> None:
self._session: dict[str, SessionStorage] = {} self._session: dict[str, UserSession] = {}
def verify_token(self, token: str) -> Optional[SessionStorage]: def verify_token(self, token: str) -> Optional[UserSession]:
return self._session.get(token) return self._session.get(token)
def set_session(self, session: SessionStorage) -> str: def set_session(self, session: UserSession) -> str:
key = secrets.token_hex(32) key = secrets.token_hex(32)
self._session[key] = session self._session[key] = session
return key return key
def del_session(self, token: str) -> None: def del_session(self, token: Optional[str]) -> None:
if token is not None:
self._session.pop(token, None) self._session.pop(token, None)
@@ -45,7 +45,7 @@ class MailingService:
return f""" return f"""
Hallo {user.user_name}, Hallo {user.user_name},
deinem Account wurden {added_balance} € hinzugefügt. Dein neues Guthaben beträgt nun {total_balance} €. deinem Account wurden {added_balance:.2f} € hinzugefügt. Dein neues Guthaben beträgt nun {total_balance:.2f} €.
Wenn du zu dieser Aufladung Fragen hast, stehen wir dir in unserem Discord Server oder per Mail an {self._configuration_service.get_lan_info().organizer_mail} zur Verfügung. Wenn du zu dieser Aufladung Fragen hast, stehen wir dir in unserem Discord Server oder per Mail an {self._configuration_service.get_lan_info().organizer_mail} zur Verfügung.
@@ -15,11 +15,15 @@ class ReceiptPrintingService:
self._seating_service = seating_service self._seating_service = seating_service
self._config = config self._config = config
self._dev_mode_enabled = dev_mode_enabled self._dev_mode_enabled = dev_mode_enabled
self._url = f"http://{self._config.host}:{self._config.port}/{self._config.order_print_endpoint}"
async def print_order(self, user: User, order: CateringOrder) -> None: async def print_order(self, user: User, order: CateringOrder) -> None:
seat_id = await self._seating_service.get_user_seat(user.user_id) seat = await self._seating_service.get_user_seat(user.user_id)
if not seat_id: if seat is None:
seat_id = " - " seat_id = " - "
else:
seat_id = str(seat.seat_id)
menu_items_payload = [] menu_items_payload = []
for item, amount in order.items.items(): for item, amount in order.items.items():
@@ -35,14 +39,19 @@ class ReceiptPrintingService:
"seat_id": seat_id, "seat_id": seat_id,
"items": menu_items_payload "items": menu_items_payload
} }
logger.info(f"Sending print order to {self._url}: {payload}")
try: try:
requests.post( response = requests.post(
f"http://{self._config.host}:{self._config.port}/{self._config.order_print_endpoint}", self._url,
json=payload, json=payload,
headers={"x-password": self._config.password} headers={"x-password": self._config.password},
timeout=2.0
) )
if response.status_code != 200:
logger.error(f"Received an error with code {response.status_code}: {response.text}")
except Exception as e: except Exception as e:
if self._dev_mode_enabled: if self._dev_mode_enabled:
logger.info("An error occurred trying to print a receipt:", e) logger.info("An error occurred trying to print a receipt: %s", e)
return return
logger.error("An error occurred trying to print a receipt:", e) logger.error("An error occurred trying to print a receipt: %s", e)
@@ -0,0 +1,17 @@
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()
@@ -0,0 +1,134 @@
from string import ascii_letters, digits
from typing import Optional
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService, DuplicationError
from src.ezgg_lan_manager.types.Team import TeamStatus, Team
from src.ezgg_lan_manager.types.User import User
class NameNotAllowedError(Exception):
def __init__(self, disallowed_char: str) -> None:
self.disallowed_char = disallowed_char
class AlreadyMemberError(Exception):
def __init__(self) -> None:
pass
class NotMemberError(Exception):
def __init__(self) -> None:
pass
class TeamLeadRemovalError(Exception):
def __init__(self) -> None:
pass
class TeamNameTooLongError(Exception):
def __init__(self) -> None:
pass
class TeamNameAlreadyTaken(Exception):
def __init__(self) -> None:
pass
class TeamAbbrInvalidError(Exception):
def __init__(self) -> None:
pass
class TeamService:
ALLOWED_TEAM_NAME_SYMBOLS = ascii_letters + digits + "!#$%&*+,-./:;<=>?[]^_{|}~ "
MAX_TEAM_NAME_LENGTH = 24
MAX_TEAM_ABBR_LENGTH = 8
def __init__(self, db_service: DatabaseService) -> None:
self._db_service = db_service
async def get_all_teams(self) -> list[Team]:
return await self._db_service.get_teams()
async def get_team_by_id(self, team_id: int) -> Optional[Team]:
return await self._db_service.get_team_by_id(team_id)
async def get_teams_for_user_by_id(self, user_id: int) -> list[Team]:
all_teams = await self.get_all_teams()
user_teams = []
for team in all_teams:
if user_id in [u.user_id for u in team.members.keys()]:
user_teams.append(team)
return user_teams
async def create_team(self, team_name: str, team_abbr: str, join_password: str, leader: User) -> Team:
disallowed_char = self._check_for_disallowed_char(team_name)
if disallowed_char:
raise NameNotAllowedError(disallowed_char)
disallowed_char = self._check_for_disallowed_char(team_abbr)
if disallowed_char:
raise NameNotAllowedError(disallowed_char)
if not team_name or len(team_name) > self.MAX_TEAM_NAME_LENGTH:
raise TeamNameTooLongError()
if not team_abbr or len(team_abbr) > self.MAX_TEAM_ABBR_LENGTH:
raise TeamAbbrInvalidError()
try:
created_team = await self._db_service.create_team(team_name, team_abbr, join_password, leader)
except DuplicationError:
raise TeamNameAlreadyTaken
return created_team
async def update_team(self, team: Team) -> Team:
"""
Updates the team EXCLUDING adding and removing members. This is to be done via add_member_to_team and remove_member_from_team
:param team: New instance of Team that is to be updated
:return: The modified Team instance
"""
disallowed_char = self._check_for_disallowed_char(team.name)
if disallowed_char:
raise NameNotAllowedError(disallowed_char)
disallowed_char = self._check_for_disallowed_char(team.abbreviation)
if disallowed_char:
raise NameNotAllowedError(disallowed_char)
if not team.name or len(team.name) > self.MAX_TEAM_NAME_LENGTH:
raise TeamNameTooLongError()
if not team.abbreviation or len(team.abbreviation) > self.MAX_TEAM_ABBR_LENGTH:
raise TeamAbbrInvalidError()
return await self._db_service.update_team(team)
async def add_member_to_team(self, team: Team, user: User, status: TeamStatus = TeamStatus.MEMBER) -> Team:
if user in team.members:
raise AlreadyMemberError()
await self._db_service.add_member_to_team(team, user, status)
return await self.get_team_by_id(team.id)
async def remove_member_from_team(self, team: Team, user: User) -> Team:
if user not in team.members:
raise NotMemberError()
if team.members[user] == TeamStatus.LEADER:
raise TeamLeadRemovalError()
await self._db_service.remove_user_from_team(team, user)
return await self.get_team_by_id(team.id)
async def is_join_password_valid(self, team_id: int, join_password: str) -> bool:
team = await self.get_team_by_id(team_id)
if not team:
return False
return team.join_password == join_password
def _check_for_disallowed_char(self, name: str) -> Optional[str]:
for c in name:
if c not in self.ALLOWED_TEAM_NAME_SYMBOLS:
return c
return None
@@ -1,8 +1,13 @@
import json
from pprint import pprint
from typing import Optional 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.DatabaseService import DatabaseService
from src.ezgg_lan_manager.services.UserService import UserService from src.ezgg_lan_manager.services.UserService import UserService
from src.ezgg_lan_manager.types.Participant import Participant 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.Tournament import Tournament
from src.ezgg_lan_manager.types.TournamentBase import ParticipantType, TournamentError from src.ezgg_lan_manager.types.TournamentBase import ParticipantType, TournamentError
from src.ezgg_lan_manager.types.User import User from src.ezgg_lan_manager.types.User import User
@@ -17,6 +22,10 @@ class TournamentService:
self._cache: dict[int, Tournament] = {} self._cache: dict[int, Tournament] = {}
self._cache_dirty: bool = True # Setting this flag invokes cache update on next read 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: async def _update_cache(self) -> None:
tournaments = await self._db_service.get_all_tournaments() tournaments = await self._db_service.get_all_tournaments()
for tournament in tournaments: for tournament in tournaments:
@@ -27,11 +36,24 @@ class TournamentService:
tournament = await self.get_tournament_by_id(tournament_id) tournament = await self.get_tournament_by_id(tournament_id)
if not tournament: if not tournament:
raise TournamentError(f"No tournament with ID {tournament_id} was found") 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) participant = Participant(id_=user_id, participant_type=ParticipantType.PLAYER)
tournament.add_participant(participant) tournament.add_participant(participant)
await self._db_service.add_participant_to_tournament(participant, tournament) await self._db_service.add_participant_to_tournament(participant, tournament)
self._cache_dirty = True 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: async def unregister_user_from_tournament(self, user_id: int, tournament_id: int) -> None:
tournament = await self.get_tournament_by_id(tournament_id) tournament = await self.get_tournament_by_id(tournament_id)
if not tournament: if not tournament:
@@ -42,6 +64,16 @@ class TournamentService:
await self._db_service.remove_participant_from_tournament(participant, tournament) await self._db_service.remove_participant_from_tournament(participant, tournament)
self._cache_dirty = True 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]: async def get_tournaments(self) -> list[Tournament]:
if self._cache_dirty: if self._cache_dirty:
await self._update_cache() await self._update_cache()
@@ -57,16 +89,56 @@ class TournamentService:
participant_ids = [p.id for p in participants] participant_ids = [p.id for p in participants]
return list(filter(lambda u: u.user_id in participant_ids, all_users)) 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): async def start_tournament(self, tournament_id: int):
tournament = await self.get_tournament_by_id(tournament_id) tournament = await self.get_tournament_by_id(tournament_id)
if tournament: if tournament:
tournament.start() tournament.start()
# ToDo: Write matches/round to database await self._generate_initial_json_file(tournament)
await self._db_service.change_tournament_status(tournament_id, tournament.status)
self._cache_dirty = True self._cache_dirty = True
async def cancel_tournament(self, tournament_id: int): async def cancel_tournament(self, tournament_id: int):
tournament = await self.get_tournament_by_id(tournament_id) tournament = await self.get_tournament_by_id(tournament_id)
if tournament: if tournament:
tournament.cancel() tournament.cancel()
# ToDo: Update to database await self._db_service.change_tournament_status(tournament_id, tournament.status)
self._cache_dirty = True 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
+4 -1
View File
@@ -59,9 +59,12 @@ class UserService:
async def is_login_valid(self, user_name_or_mail: str, password_clear_text: str) -> bool: async def is_login_valid(self, user_name_or_mail: str, password_clear_text: str) -> bool:
user = await self.get_user(user_name_or_mail) user = await self.get_user(user_name_or_mail)
user_password_hash = sha256(password_clear_text.encode(encoding="utf-8")).hexdigest()
if not user: if not user:
return False return False
return user.user_password == sha256(password_clear_text.encode(encoding="utf-8")).hexdigest() if user.user_fallback_password and user.user_fallback_password == user_password_hash:
return True
return user.user_password == user_password_hash
def _check_for_disallowed_char(self, name: str) -> Optional[str]: def _check_for_disallowed_char(self, name: str) -> Optional[str]:
+8
View File
@@ -49,6 +49,14 @@ class Match:
games.append(Game(game_id, self._match_id, game_number, None, None, False)) games.append(Game(game_id, self._match_id, game_number, None, None, False))
return tuple(games) return tuple(games)
@property
def round_number(self) -> int:
return self._round_number
@property
def best_of(self) -> int:
return self._best_of
@property @property
def status(self) -> MatchStatus: def status(self) -> MatchStatus:
if self._status == MatchStatus.COMPLETED: if self._status == MatchStatus.COMPLETED:
@@ -1,36 +0,0 @@
import logging
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Optional
logger = logging.getLogger(__name__.split(".")[-1])
# ToDo: Persist between reloads: https://rio.dev/docs/howto/persistent-settings
# Note for ToDo: rio.UserSettings are saved LOCALLY, do not just read a user_id here!
@dataclass(frozen=False)
class SessionStorage:
_user_id: Optional[int] = None # DEBUG: Put user ID here to skip login
_is_team_member: bool = False
_notification_callbacks: dict[str, Callable] = field(default_factory=dict)
async def clear(self) -> None:
await self.set_user_id_and_team_member_flag(None, False)
def subscribe_to_logged_in_or_out_event(self, component_id: str, callback: Callable) -> None:
self._notification_callbacks[component_id] = callback
@property
def user_id(self) -> Optional[int]:
return self._user_id
@property
def is_team_member(self) -> bool:
return self._is_team_member
async def set_user_id_and_team_member_flag(self, user_id: Optional[int], is_team_member: bool) -> None:
self._user_id = user_id
self._is_team_member = is_team_member
for component_id, callback in self._notification_callbacks.items():
logger.debug(f"Calling logged in callback from {component_id}")
await callback()
+37
View File
@@ -0,0 +1,37 @@
from dataclasses import dataclass
from enum import Enum
from typing import Self
from src.ezgg_lan_manager.types.User import User
class TeamStatus(Enum):
MEMBER = 0
OFFICER = 1
LEADER = 2
@classmethod
def from_str(cls, team_status: str) -> Self:
if team_status == "MEMBER":
return TeamStatus.MEMBER
elif team_status == "OFFICER":
return TeamStatus.OFFICER
elif team_status == "LEADER":
return TeamStatus.LEADER
raise ValueError
@dataclass(frozen=True)
class Team:
id: int
name: str
abbreviation: str
members: dict[User, TeamStatus]
join_password: str
def __hash__(self) -> int:
return hash(self.id)
def __eq__(self, other):
if not isinstance(other, Team):
return NotImplemented
return self.id == other.id
+11 -2
View File
@@ -1,3 +1,4 @@
import logging
import uuid import uuid
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
@@ -5,8 +6,9 @@ from math import ceil, log2
from src.ezgg_lan_manager.types.Match import Match, FFAMatch 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.Participant import Participant
from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, TournamentError, Bracket, MatchStatus from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, TournamentError, Bracket, MatchStatus, ParticipantType
logger = logging.getLogger(__name__.split(".")[-1])
class Tournament: class Tournament:
def __init__(self, def __init__(self,
@@ -20,7 +22,8 @@ class Tournament:
participants: list[Participant], participants: list[Participant],
matches: Optional[tuple[Match]], matches: Optional[tuple[Match]],
rounds: list[list[Match]], rounds: list[list[Match]],
max_participants: int) -> None: max_participants: int,
participant_type: ParticipantType) -> None:
self._id = id_ self._id = id_
self._name = name self._name = name
self._description = description self._description = description
@@ -32,6 +35,7 @@ class Tournament:
self._matches = matches self._matches = matches
self._rounds = rounds self._rounds = rounds
self._max_participants = max_participants self._max_participants = max_participants
self._participant_type = participant_type
@property @property
def id(self) -> int: def id(self) -> int:
@@ -85,6 +89,10 @@ class Tournament:
def is_full(self) -> bool: def is_full(self) -> bool:
return len(self._participants) >= self._max_participants return len(self._participants) >= self._max_participants
@property
def participant_type(self) -> ParticipantType:
return self._participant_type
def add_participant(self, participant: Participant) -> None: def add_participant(self, participant: Participant) -> None:
if participant.id in (p.id for p in self._participants): if participant.id in (p.id for p in self._participants):
raise TournamentError(f"Participant with ID {participant.id} already registered for tournament") raise TournamentError(f"Participant with ID {participant.id} already registered for tournament")
@@ -347,6 +355,7 @@ class Tournament:
raise TournamentError(f"Unknown bracket type: {bracket_type}") raise TournamentError(f"Unknown bracket type: {bracket_type}")
self._status = TournamentStatus.ONGOING self._status = TournamentStatus.ONGOING
logger.info(f"New tournament status for {self._name}: {self._status}")
for match in self._matches: for match in self._matches:
match.check_completion() match.check_completion()
+7 -1
View File
@@ -9,6 +9,7 @@ class User:
user_name: str user_name: str
user_mail: str user_mail: str
user_password: str user_password: str
user_fallback_password: Optional[str]
user_first_name: Optional[str] user_first_name: Optional[str]
user_last_name: Optional[str] user_last_name: Optional[str]
user_birth_day: Optional[date] user_birth_day: Optional[date]
@@ -19,4 +20,9 @@ class User:
last_updated_at: datetime last_updated_at: datetime
def __hash__(self) -> int: def __hash__(self) -> int:
return hash(f"{self.user_id}{self.user_name}{self.user_mail}") return hash(self.user_id)
def __eq__(self, other):
if not isinstance(other, User):
return NotImplemented
return self.user_id == other.user_id
@@ -0,0 +1,9 @@
from uuid import UUID
from rio import Dataclass
class UserSession(Dataclass):
id: UUID
user_id: int
is_team_member: bool
+1
View File
@@ -0,0 +1 @@
*.json
+43
View File
@@ -0,0 +1,43 @@
# 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