68 Commits

Author SHA1 Message Date
David Rodenkirchen e92dc25ade wip / do not merge 2026-04-16 07:26:50 +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
David Rodenkirchen 2f4c3a15b7 bump version to 0.2.0 2026-02-04 00:03:04 +01:00
Typhus 54df84a7da Add Tournaments UI (#32)
Co-authored-by: David Rodenkirchen <drodenkirchen@linetco.com>
Reviewed-on: #32
2026-02-03 23:00:58 +00:00
David Rodenkirchen ff5d715a4e add tournament data model 2026-01-28 20:14:40 +00:00
David Rodenkirchen b505191156 add hover tooltips for seating plan 2025-11-12 23:36:33 +01:00
David Rodenkirchen 43559fcf30 Rename EZ-LAN to EZGG-LAN 2025-07-30 22:38:57 +02:00
David Rodenkirchen af8cf19dc8 send mail to user when account balances is added to 2025-07-30 13:33:55 +02:00
David Rodenkirchen 29caadaca2 rename lan 2025-07-26 14:16:09 +02:00
Typhus 6e598b577f Merge pull request 'upgrade rio to 0.11.2rc6' (#21) from feature/upgrade-rio-rc into main
Reviewed-on: Vereins-IT/ez-lan-manager#21
2025-07-13 21:37:48 +00:00
David Rodenkirchen 1091179492 upgrade rio to 0.11.2rc6 2025-07-13 23:37:17 +02:00
David Rodenkirchen 530efcd286 make portrait mode optional 2025-05-13 07:10:24 +02:00
David Rodenkirchen 4018502a63 fix seating plan 2025-04-03 22:54:20 +02:00
David Rodenkirchen 3f06015cb8 add 502 Fallback HTML 2025-04-03 17:59:58 +02:00
David Rodenkirchen 336bd0e108 Introduce receipt printing 2025-04-03 06:53:58 +02:00
David Rodenkirchen fa90e16fdf register fix 2025-03-27 08:01:51 +01:00
David Rodenkirchen b987623d9d sql update 2025-03-27 07:54:52 +01:00
David Rodenkirchen f58a7872ef periodically refresh order status 2025-03-26 00:07:04 +01:00
David Rodenkirchen 23e903a207 add additional info to seating plan 2025-03-25 23:59:10 +01:00
David Rodenkirchen 8aee3af9d7 dont empty cart if funds insufficient 2025-03-25 23:34:55 +01:00
David Rodenkirchen dd8b79c254 fix seating plan info box not showing correct button 2025-03-25 23:28:32 +01:00
David Rodenkirchen 2f6e2b15aa minor fixes 2025-03-25 23:19:25 +01:00
David Rodenkirchen b4da6b9729 fix bug where contact page did not respond 2025-03-25 23:04:39 +01:00
David Rodenkirchen a430c81624 make account balance string formatting more rigid 2025-03-25 21:10:39 +01:00
David Rodenkirchen 1d21fbae5a improve seating plan booking button 2025-03-25 21:05:17 +01:00
David Rodenkirchen db8ada283b make UserInfoBox rigid against missing userid 2025-03-25 20:19:26 +01:00
David Rodenkirchen d3aadcf697 add overview page 2025-03-24 10:37:26 +01:00
David Rodenkirchen bbb4e8f1d1 remove seating from configuration 2025-03-24 08:31:23 +01:00
David Rodenkirchen 51b07baa36 add SQL for catering menu items 2025-03-23 23:57:23 +01:00
David Rodenkirchen ea9a805483 add conditional footer width workaround 2025-03-23 23:57:08 +01:00
David Rodenkirchen d43dd96fbb Change Seating Plan to Donsbach 2025-03-23 23:56:49 +01:00
David Rodenkirchen a61ee8e775 enable Discord Link 2025-03-23 23:56:27 +01:00
David Rodenkirchen 328bea32d7 bump version to 0.1.0 2025-03-23 23:56:05 +01:00
David Rodenkirchen 49d89feda3 remove buggy input checks 2025-03-23 21:48:44 +01:00
David Rodenkirchen 26994d4536 dockerize 2025-03-23 17:20:14 +01:00
David Rodenkirchen ee6f35b771 add development instructions to README 2025-03-11 22:26:32 +01:00
tcprod 40f8bc1049 Merge pull request 'bugfix/rounding-taxes' (#10) from bugfix/rounding-taxes into main
Reviewed-on: Vereins-IT/ez-lan-manager#10
Reviewed-by: David Rodenkirchen <typhus@ezgg-ev.de>
2025-02-07 22:31:50 +00:00
tcprod a419ee8885 Replace float with Decimal for price calculations
Fix Decimal precision issue

Fix Decimal precision issue

Fix Decimal precision issue

Fix old prices for tickets

Fix Decimal precision issue
2025-02-07 23:20:57 +01:00
Typhus 98c2d1570c improve catering management popup styling (#9)
<img width="562" alt="image" src="attachments/62b290bc-dbb3-4760-8e88-b8d9e6fe1263">

Co-authored-by: David Rodenkirchen <drodenkirchen@linetco.com>
Reviewed-on: Vereins-IT/ez-lan-manager#9
2025-02-04 08:13:02 +00:00
Typhus 8971f4b73f improve catering management (#8)
Co-authored-by: David Rodenkirchen <drodenkirchen@linetco.com>
Reviewed-on: Vereins-IT/ez-lan-manager#8
2025-02-04 08:00:29 +00:00
139 changed files with 6210 additions and 1511 deletions
+22
View File
@@ -0,0 +1,22 @@
<html>
<head>
<title>EZGG LAN Manager - Wartungsmodus</title>
<style>
body { text-align: center; padding: 150px; }
h1 { font-size: 50px; }
body { font: 20px Helvetica, sans-serif; color: #333; }
article { display: block; text-align: left; width: 650px; margin: 0 auto; }
a { color: #dc8100; text-decoration: none; }
a:hover { color: #333; text-decoration: none; }
</style>
</head>
<body>
<article>
<h1>Wir sind bald wieder da!</h1>
<div>
<p>Wir f&uuml;hren zurzeit Wartungsarbeiten durch und sind in k&uuml;rze wieder f&uuml;r euch da.</p>
<p>&mdash; Euer EZGG LAN Team</p>
</div>
</article>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
FROM python:3.12-bookworm
RUN apt-get update
RUN apt install dumb-init
COPY requirements.txt .
RUN pip install -r requirements.txt
EXPOSE 8000
EXPOSE 8001
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
+42 -6
View File
@@ -1,13 +1,49 @@
# EZ LAN Manager
# EZGG LAN Manager
## Overview
This repository contains the code for the EZ LAN Manager.
This repository contains the code for the EZGG LAN Manager.
## How to install [prod]
## Development Setup
TBD
### Prerequisites
## How to install [dev]
- Working Installation of MariaDB Server (version `10.6.25` or later)
+ MySQL should work too, but there are no guarantees.
- Python 3.9 or higher
- PyCharm or similar IDE (optional)
TBD
### Step 1: Preparing Database
To prepare the database, apply the SQL file located in `sql/create_database.sql` to your database server. This is easily accomplished with the MYSQL Workbench, but it can be also done by piping the file into the mariadb-server executable.
After creating the database, apply all patches found in `sql/*_patch.sql` in their numeric order.
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.
### Step 2: Preparing configuration
Use the example configuration at `config/config.example.toml` to create a `config.toml` at the base of the repository. Most of the parameters do not matter to get the development setup done, but the database credentials need to be correct.
### Step 3: Install dependecies
Use `pip install -r requirements.txt` to install the requirements. The usage of a venv is recommended.
### Step 4: Running the application
Run the application by executing the file `EzggLanManager.py` found at `src/ezgg_lan_manager`. Check the STDOUT for information regarding the port on which the application is now served.
## Docker Deployment
To get the docker compose setup running, you need to manually complete the following steps:
1. Create a valid `config.toml` in the project root, so it gets copied over into the container.
2. Create the database user:
```sql
CREATE USER 'ezgg_lan_manager'@'%' IDENTIFIED BY 'PASSWORD';
GRANT ALL PRIVILEGES ON ezgg_lan_manager.* TO 'ezgg_lan_manager'@'%';
FLUSH PRIVILEGES;
```
3. Make sure to **NOT** use the default passwords!
4. Apply the `create_database.sql` when starting the MariaDB container for the first time.
5. Apply the patches (`sql/*_patch.sql`) when starting the MariaDB container for the first time.
+1 -1
View File
@@ -1 +1 @@
0.0.1
0.3.6
+10 -10
View File
@@ -1,5 +1,5 @@
[lan]
name="EZ LAN"
name="EZGG LAN"
iteration="0.5"
date_from="2024-10-30 15:00:00"
date_till="2024-11-01 12:00:00"
@@ -10,7 +10,7 @@
db_password="demo_password"
db_host="127.0.0.1"
db_port=3306
db_name="ez_lan_manager"
db_name="ezgg_lan_manager"
[mailing]
smtp_server=""
@@ -19,26 +19,26 @@
username=""
password=""
[seating]
# SeatID -> Category
A01 = "NORMAL"
A02 = "NORMAL"
C01 = "LUXUS"
[tickets]
[tickets."NORMAL"]
total_tickets=30
price=2500
price="25.00"
description="Normales Ticket"
additional_info="Berechtigt zur Nutzung eines regulären Platzes für die gesamte Dauer der LAN"
is_default=true
[tickets."LUXUS"]
total_tickets=10
price=3500
price="35.00"
description="Luxus Ticket"
additional_info="Berechtigt zur Nutzung eines verbesserten Platzes. Dieser ist mit einer höheren Internet-Bandbreite und einem Sitzkissen ausgestattet."
is_default=false
[receipt_printing]
host="127.0.0.1"
port="5000"
order_print_endpoint="print_order"
password="Alkohol1"
[misc]
dev_mode_active=true # Supresses E-Mail sending
+37
View File
@@ -0,0 +1,37 @@
services:
lan_manager:
build: .
depends_on:
db:
condition: service_healthy
environment:
PYTHONPATH: /opt/ezgg-lan-manager
ports:
- "8000:8000"
- "8001:8001"
volumes:
- ./:/opt/ezgg-lan-manager
entrypoint: ["/bin/sh", "-c", "cd /opt/ezgg-lan-manager/src && python3 /opt/ezgg-lan-manager/src/EzggLanManager.py"]
db:
image: mariadb:latest
environment:
MARIADB_ROOT_PASSWORD: Alkohol1
MARIADB_DATABASE: ezgg_lan_manager
MARIADB_USER: ezgg_lan_manager
MARIADB_PASSWORD: Alkohol1
healthcheck:
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost", "-pAlkohol1"]
interval: 5s
timeout: 3s
retries: 5
ports:
- "127.0.0.1:3306:3306"
volumes:
- database:/var/lib/mysql
- ./sql/create_database.sql:/docker-entrypoint-initdb.d/init.sql
- ./sql:/sql
volumes:
database:
BIN
View File
Binary file not shown.
+144
View File
@@ -0,0 +1,144 @@
-- Apply this patch after using create_database.sql to extend the schema to support tournaments from version 0.2.0
-- WARNING: Executing this on a post 0.2.0 database will delete all data related to tournaments !!!
DROP TABLE IF EXISTS `game_titles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `game_titles` (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
web_link VARCHAR(512) NOT NULL,
image_name VARCHAR(255) NOT NULL,
UNIQUE KEY uq_game_title_name (name)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
DROP TABLE IF EXISTS `tournaments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `tournaments` (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
game_title_id INT NOT NULL,
format VARCHAR(20) NOT NULL, -- SE_BO1, DE_BO3, ...
start_time DATETIME NOT NULL,
status VARCHAR(20) NOT NULL, -- OPEN, CLOSED, ONGOING, ...
max_participants INT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_tournament_game
FOREIGN KEY (game_title_id)
REFERENCES game_titles(id)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
CREATE INDEX idx_tournaments_game_title
ON tournaments(game_title_id);
DROP TABLE IF EXISTS `tournament_participants`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `tournament_participants` (
id INT AUTO_INCREMENT PRIMARY KEY,
tournament_id INT NOT NULL,
user_id INT NOT NULL,
participant_type VARCHAR(10) NOT NULL DEFAULT 'PLAYER',
seed INT NULL,
joined_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_tournament_user (tournament_id, user_id),
CONSTRAINT fk_tp_tournament
FOREIGN KEY (tournament_id)
REFERENCES tournaments(id)
ON DELETE CASCADE,
CONSTRAINT fk_tp_user
FOREIGN KEY (user_id)
REFERENCES users(user_id)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
CREATE INDEX idx_tp_tournament
ON tournament_participants(tournament_id);
CREATE INDEX idx_tp_user
ON tournament_participants(user_id);
DROP TABLE IF EXISTS `tournament_rounds`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `tournament_rounds` (
id INT AUTO_INCREMENT PRIMARY KEY,
tournament_id INT NOT NULL,
bracket VARCHAR(10) NOT NULL, -- UPPER, LOWER, FINAL
round_index INT NOT NULL,
UNIQUE KEY uq_round (tournament_id, bracket, round_index),
CONSTRAINT fk_round_tournament
FOREIGN KEY (tournament_id)
REFERENCES tournaments(id)
ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
CREATE INDEX idx_rounds_tournament
ON tournament_rounds(tournament_id);
DROP TABLE IF EXISTS `matches`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `matches` (
id INT AUTO_INCREMENT PRIMARY KEY,
tournament_id INT NOT NULL,
round_id INT NOT NULL,
match_index INT NOT NULL,
status VARCHAR(15) NOT NULL, -- WAITING, PENDING, COMPLETED, ...
best_of INT NOT NULL, -- 1, 3, 5
scheduled_time DATETIME NULL,
completed_at DATETIME NULL,
UNIQUE KEY uq_match (round_id, match_index),
CONSTRAINT fk_match_tournament
FOREIGN KEY (tournament_id)
REFERENCES tournaments(id)
ON DELETE CASCADE,
CONSTRAINT fk_match_round
FOREIGN KEY (round_id)
REFERENCES tournament_rounds(id)
ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
CREATE INDEX idx_matches_tournament
ON matches(tournament_id);
CREATE INDEX idx_matches_round
ON matches(round_id);
DROP TABLE IF EXISTS `match_participants`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `match_participants` (
match_id INT NOT NULL,
participant_id INT NOT NULL,
score INT NULL,
is_winner TINYINT(1) NULL,
PRIMARY KEY (match_id, participant_id),
CONSTRAINT fk_mp_match
FOREIGN KEY (match_id)
REFERENCES matches(id)
ON DELETE CASCADE,
CONSTRAINT fk_mp_participant
FOREIGN KEY (participant_id)
REFERENCES tournament_participants(id)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
+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;
+65
View File
@@ -0,0 +1,65 @@
INSERT INTO `catering_menu_items` VALUES
(1,'Schnitzel Wiener Art','mit Pommes Frites und Salat',20.50,'MAIN_COURSE',1),
(2,'Jäger Schnitzel mit Champignonrahm Sauce','mit Spätzlen und Salat',22.50,'MAIN_COURSE',1),
(3,'Rumpsteak 250g','mit Pommes Frites und Salat',35.50,'MAIN_COURSE',1),
(4,'Rindergeschnetzeltes','mit Spätzlen und Salat',22.50,'MAIN_COURSE',1),
(5,'Galloway Burger','mit Amazing Fries',20.00,'MAIN_COURSE',1),
(6,'Käsespätzle mit Röstzwiebel','mit Salat',20.00,'MAIN_COURSE',1),
(11,'Käse Schinken Wrap','',5.00,'SNACK',1),
(12,'Puten Paprika Wrap','',7.00,'SNACK',1),
(13,'Tomate Mozzarella Wrap','',6.00,'SNACK',1),
(14,'Portion Pommes','',4.00,'SNACK',1),
(15,'Rinds-Currywurst','',4.50,'SNACK',1),
(16,'Rinds-Currywurst mit Pommes','',6.50,'SNACK',1),
(17,'Nudelsalat','',4.50,'SNACK',1),
(18,'Nudelsalat mit Bockwurst','',6.00,'SNACK',1),
(19,'Kartoffelsalat','',4.50,'SNACK',1),
(20,'Kartoffelsalat mit Bockwurst','',6.00,'SNACK',1),
(21,'Sandwichtoast - Schinken','',1.80,'SNACK',1),
(22,'Sandwichtoast - Käse','',1.80,'SNACK',1),
(23,'Sandwichtoast - Schinken/Käse','',2.10,'SNACK',1),
(24,'Sandwichtoast - Salami','',1.80,'SNACK',1),
(25,'Sandwichtoast - Salami/Käse','',2.10,'SNACK',1),
(26,'Chips - Western Style','',1.50,'SNACK',1),
(27,'Nachos - Salted','',1.50,'SNACK',1),
(28,'Panna Cotta mit Erdbeersauce','',7.00,'DESSERT',1),
(29,'Panna Cotta mit Blaubeersauce','',7.00,'DESSERT',1),
(30,'Mousse au Chocolat','',7.00,'DESSERT',1),
(31,'Fruit Loops','',1.50,'BREAKFAST',1),
(32,'Smacks','',1.50,'BREAKFAST',1),
(33,'Knuspermüsli','Schoko',2.00,'BREAKFAST',1),
(34,'Cini Minis','',1.50,'BREAKFAST',1),
(35,'Brötchen - Schinken','mit Margarine',1.20,'BREAKFAST',1),
(36,'Brötchen - Käse','mit Margarine',1.20,'BREAKFAST',1),
(37,'Brötchen - Schinken/Käse','mit Margarine',1.40,'BREAKFAST',1),
(38,'Brötchen - Salami','mit Margarine',1.20,'BREAKFAST',1),
(39,'Brötchen - Salami/Käse','mit Margarine',1.40,'BREAKFAST',1),
(40,'Brötchen - Nutella','mit Margarine',1.20,'BREAKFAST',1),
(41,'Wasser - Still','1L Flasche',2.00,'BEVERAGE_NON_ALCOHOLIC',1),
(42,'Wasser - Medium','1L Flasche',2.00,'BEVERAGE_NON_ALCOHOLIC',1),
(43,'Wasser - Spritzig','1L Flasche',2.00,'BEVERAGE_NON_ALCOHOLIC',1),
(44,'Coca-Cola','1L Flasche',2.00,'BEVERAGE_NON_ALCOHOLIC',1),
(45,'Coca-Cola Zero','1L Flasche',2.00,'BEVERAGE_NON_ALCOHOLIC',1),
(46,'Fanta','1L Flasche',2.00,'BEVERAGE_NON_ALCOHOLIC',1),
(47,'Sprite','1L Flasche',2.00,'BEVERAGE_NON_ALCOHOLIC',1),
(48,'Spezi','von Paulaner, 0,5L Flasche',1.50,'BEVERAGE_NON_ALCOHOLIC',1),
(49,'Red Bull','',2.00,'BEVERAGE_NON_ALCOHOLIC',1),
(50,'Energy','Hausmarke',1.50,'BEVERAGE_NON_ALCOHOLIC',1),
(51,'Pils','0,33L Flasche',1.90,'BEVERAGE_ALCOHOLIC',1),
(52,'Radler','0,33L Flasche',1.90,'BEVERAGE_ALCOHOLIC',1),
(53,'Diesel','0,33L Flasche',1.90,'BEVERAGE_ALCOHOLIC',1),
(54,'Apfelwein Pur','0,33L Flasche',1.90,'BEVERAGE_ALCOHOLIC',1),
(55,'Apfelwein Sauer','0,33L Flasche',1.90,'BEVERAGE_ALCOHOLIC',1),
(56,'Apfelwein Cola','0,33L Flasche',1.90,'BEVERAGE_ALCOHOLIC',1),
(57,'Vodka Energy','',4.00,'BEVERAGE_COCKTAIL',1),
(58,'Vodka O-Saft','',4.00,'BEVERAGE_COCKTAIL',1),
(59,'Whiskey Cola','mit Bourbon',4.00,'BEVERAGE_COCKTAIL',1),
(60,'Jägermeister Energy','',4.00,'BEVERAGE_COCKTAIL',1),
(61,'Sex on the Beach','',5.50,'BEVERAGE_COCKTAIL',1),
(62,'Long Island Ice Tea','',5.50,'BEVERAGE_COCKTAIL',1),
(63,'Caipirinha','',5.50,'BEVERAGE_COCKTAIL',1),
(64,'Jägermeister','',2.00,'BEVERAGE_SHOT',1),
(65,'Tequila','',2.00,'BEVERAGE_SHOT',1),
(66,'Pfeffi','Pfefferminz-Schnaps',1.50,'BEVERAGE_SHOT',1),
(67,'Zigaretten','Elixyr',8.00,'NON_FOOD',1),
(68,'Mentholfilter','passend für Elixyr',1.20,'NON_FOOD',1);
+4 -4
View File
@@ -1,8 +1,8 @@
CREATE DATABASE IF NOT EXISTS `ez_lan_manager` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci */;
USE `ez_lan_manager`;
CREATE DATABASE IF NOT EXISTS `ezgg_lan_manager` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci */;
USE `ezgg_lan_manager`;
-- MySQL dump 10.13 Distrib 5.7.24, for Linux (x86_64)
--
-- Host: 127.0.0.1 Database: ez_lan_manager
-- Host: 127.0.0.1 Database: ezgg_lan_manager
-- ------------------------------------------------------
-- Server version 5.5.5-10.11.8-MariaDB-0ubuntu0.24.04.1
@@ -28,7 +28,7 @@ CREATE TABLE `catering_menu_items` (
`catering_menu_item_id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
`additional_info` varchar(300) DEFAULT '',
`price` int(11) NOT NULL DEFAULT 0,
`price` varchar(45) NOT NULL DEFAULT '0',
`category` varchar(80) NOT NULL,
`is_disabled` tinyint(4) DEFAULT 0,
PRIMARY KEY (`catering_menu_item_id`)
File diff suppressed because one or more lines are too long
+61
View File
@@ -0,0 +1,61 @@
INSERT INTO `seats` (`seat_id`, `is_blocked`, `seat_category`) VALUES
('A01', '0', 'NORMAL'),
('A02', '0', 'NORMAL'),
('A03', '0', 'NORMAL'),
('A04', '0', 'NORMAL'),
('A05', '0', 'NORMAL'),
('A06', '0', 'NORMAL'),
('A10', '0', 'NORMAL'),
('A11', '0', 'NORMAL'),
('A12', '0', 'NORMAL'),
('A13', '0', 'NORMAL'),
('A14', '0', 'NORMAL'),
('A15', '0', 'NORMAL'),
('B01', '0', 'NORMAL'),
('B02', '0', 'NORMAL'),
('B03', '0', 'NORMAL'),
('B04', '0', 'NORMAL'),
('B05', '0', 'NORMAL'),
('B10', '0', 'NORMAL'),
('B11', '0', 'NORMAL'),
('B12', '0', 'NORMAL'),
('B13', '0', 'NORMAL'),
('B14', '0', 'NORMAL'),
('C01', '0', 'NORMAL'),
('C02', '0', 'NORMAL'),
('C03', '0', 'NORMAL'),
('C04', '0', 'NORMAL'),
('C05', '0', 'NORMAL'),
('C10', '0', 'NORMAL'),
('C11', '0', 'NORMAL'),
('C12', '0', 'NORMAL'),
('C13', '0', 'NORMAL'),
('C14', '0', 'NORMAL'),
('D01', '0', 'NORMAL'),
('D02', '0', 'NORMAL'),
('D03', '0', 'NORMAL'),
('D04', '0', 'NORMAL'),
('D05', '0', 'NORMAL'),
('D06', '0', 'NORMAL'),
('D10', '0', 'NORMAL'),
('D11', '0', 'NORMAL'),
('D12', '0', 'NORMAL'),
('D13', '0', 'NORMAL'),
('D14', '0', 'NORMAL'),
('D15', '0', 'NORMAL'),
('E01', '0', 'NORMAL'),
('E02', '0', 'NORMAL'),
('E03', '0', 'NORMAL'),
('E04', '0', 'NORMAL'),
('E05', '0', 'NORMAL'),
('E06', '0', 'NORMAL'),
('E10', '0', 'NORMAL'),
('E11', '0', 'NORMAL'),
('E12', '0', 'NORMAL'),
('E13', '0', 'NORMAL'),
('E14', '0', 'NORMAL'),
('E15', '0', 'NORMAL');
+51 -15
View File
@@ -1,20 +1,19 @@
import logging
from asyncio import get_event_loop
import sys
from pathlib import Path
from uuid import uuid4
from rio import App, Theme, Color, Font, ComponentPage, Session
from from_root import from_root
from src.ez_lan_manager import pages, init_services
from src.ez_lan_manager.helpers.LoggedInGuard import logged_in_guard, not_logged_in_guard, team_guard
from src.ez_lan_manager.services.DatabaseService import NoDatabaseConnectionError
from src.ez_lan_manager.services.LocalDataService import LocalData
from src.ez_lan_manager.types.SessionStorage import SessionStorage
from src.ezgg_lan_manager import pages, init_services, LocalDataService
from src.ezgg_lan_manager.helpers.LoggedInGuard import logged_in_guard, not_logged_in_guard, team_guard
from src.ezgg_lan_manager.services.LocalDataService import LocalData
from src.ezgg_lan_manager.types.UserSession import UserSession
logger = logging.getLogger("EzLanManager")
logger = logging.getLogger("EzggLanManager")
if __name__ == "__main__":
theme = Theme.from_colors(
@@ -28,16 +27,24 @@ if __name__ == "__main__":
corner_radius_small=0,
corner_radius_medium=0,
corner_radius_large=0,
font=Font(from_root("src/ez_lan_manager/assets/fonts/joystix.otf"))
font=Font(
regular=from_root("src/ezgg_lan_manager/assets/fonts/PixelOperatorMono.ttf"),
bold=from_root("src/ezgg_lan_manager/assets/fonts/PixelOperatorMono-Bold.ttf"),
)
)
default_attachments = [LocalData()]
default_attachments: list = [LocalData(stored_session_token=None)]
default_attachments.extend(init_services())
lan_info = default_attachments[3].get_lan_info()
async def on_session_start(session: Session) -> None:
# Use this line to fake being any user without having to log in
# session.attach(UserSession(id=uuid4(), user_id=30, is_team_member=True))
await session.set_title(lan_info.name)
session.attach(SessionStorage())
if session[LocalData].stored_session_token:
user_session = session[LocalDataService].verify_token(session[LocalData].stored_session_token)
if user_session is not None:
session.attach(user_session)
async def on_app_start(a: App) -> None:
init_result = await a.default_attachments[4].init_db_pool()
@@ -46,7 +53,7 @@ if __name__ == "__main__":
sys.exit(1)
app = App(
name="EZ LAN Manager",
name="EZGG LAN Manager",
build=pages.BasePage,
pages=[
ComponentPage(
@@ -62,7 +69,7 @@ if __name__ == "__main__":
ComponentPage(
name="Overview",
url_segment="overview",
build=lambda: pages.PlaceholderPage(placeholder_name="LAN Übersicht"),
build=pages.OverviewPage,
),
ComponentPage(
name="BuyTicket",
@@ -157,10 +164,36 @@ if __name__ == "__main__":
build=pages.ManageTournamentsPage,
guard=team_guard
),
ComponentPage(
name="AdminNavigationPage",
url_segment="admin",
build=pages.AdminNavigationPage,
guard=team_guard
),
ComponentPage(
name="DbErrorPage",
url_segment="db-error",
build=pages.DbErrorPage,
),
ComponentPage(
name="TournamentDetailsPage",
url_segment="tournament",
build=pages.TournamentDetailsPage,
),
ComponentPage(
name="TournamentRulesPage",
url_segment="tournament-rules",
build=pages.TournamentRulesPage,
),
ComponentPage(
name="Teams",
url_segment="teams",
build=pages.TeamsPage,
),
ComponentPage(
name="ConwaysGameOfLife",
url_segment="conway",
build=pages.ConwayPage,
)
],
theme=theme,
@@ -168,13 +201,13 @@ if __name__ == "__main__":
default_attachments=default_attachments,
on_session_start=on_session_start,
on_app_start=on_app_start,
icon=from_root("src/ez_lan_manager/assets/img/favicon.png"),
icon=from_root("src/ezgg_lan_manager/assets/img/favicon.png"),
meta_tags={
"robots": "INDEX,FOLLOW",
"description": f"Info und Verwaltungs-Seite der LAN Party '{lan_info.name} - {lan_info.iteration}'.",
"og:description": f"Info und Verwaltungs-Seite der LAN Party '{lan_info.name} - {lan_info.iteration}'.",
"keywords": "Gaming, Clan, Guild, Verein, Club, Einfach, Zocken, Gesellschaft, Videospiele, "
"Videogames, LAN, Party, EZ, LAN, Manager",
"Videogames, LAN, Party, EZ, EZGG, LAN, Manager",
"author": "David Rodenkirchen",
"publisher": "EZ GG e.V.",
"copyright": "EZ GG e.V.",
@@ -186,4 +219,7 @@ if __name__ == "__main__":
}
)
sys.exit(app.run_as_web_server())
sys.exit(app.run_as_web_server(
host="0.0.0.0",
port=8000,
))
-32
View File
@@ -1,32 +0,0 @@
import logging
from from_root import from_root
from src.ez_lan_manager.services import *
from src.ez_lan_manager.services.AccountingService import AccountingService
from src.ez_lan_manager.services.CateringService import CateringService
from src.ez_lan_manager.services.ConfigurationService import ConfigurationService
from src.ez_lan_manager.services.DatabaseService import DatabaseService
from src.ez_lan_manager.services.LocalDataService import LocalDataService
from src.ez_lan_manager.services.MailingService import MailingService
from src.ez_lan_manager.services.NewsService import NewsService
from src.ez_lan_manager.services.SeatingService import SeatingService
from src.ez_lan_manager.services.TicketingService import TicketingService
from src.ez_lan_manager.services.UserService import UserService
from src.ez_lan_manager.types import *
# Inits services in the correct order
def init_services() -> tuple[AccountingService, CateringService, ConfigurationService, DatabaseService, MailingService, NewsService, SeatingService, TicketingService, UserService, LocalDataService]:
logging.basicConfig(level=logging.DEBUG)
configuration_service = ConfigurationService(from_root("config.toml"))
db_service = DatabaseService(configuration_service.get_database_configuration())
user_service = UserService(db_service)
accounting_service = AccountingService(db_service)
news_service = NewsService(db_service)
mailing_service = MailingService(configuration_service)
ticketing_service = TicketingService(configuration_service.get_ticket_info(), db_service, accounting_service)
seating_service = SeatingService(configuration_service.get_seating_configuration(), configuration_service.get_lan_info(), db_service, ticketing_service)
catering_service = CateringService(db_service, accounting_service, user_service)
local_data_service = LocalDataService()
return accounting_service, catering_service, configuration_service, db_service, mailing_service, news_service, seating_service, ticketing_service, user_service, local_data_service
@@ -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,93 +0,0 @@
from copy import copy, deepcopy
from typing import Optional, Callable
from rio import *
from src.ez_lan_manager import ConfigurationService, UserService, LocalDataService
from src.ez_lan_manager.components.DesktopNavigationButton import DesktopNavigationButton
from src.ez_lan_manager.components.UserInfoAndLoginBox import UserInfoAndLoginBox
from src.ez_lan_manager.services.LocalDataService import LocalData
from src.ez_lan_manager.types.SessionStorage import SessionStorage
from src.ez_lan_manager.types.User import User
class DesktopNavigation(Component):
user: Optional[User] = None
force_login_box_refresh: list[Callable] = []
@event.on_populate
async def async_init(self) -> None:
self.session[SessionStorage].subscribe_to_logged_in_or_out_event(str(self.__class__), self.async_init)
local_data = self.session[LocalData]
if local_data.stored_session_token:
session_ = self.session[LocalDataService].verify_token(local_data.stored_session_token)
if session_:
self.session.detach(SessionStorage)
self.session.attach(session_)
self.user = await self.session[UserService].get_user(session_.user_id)
try:
# Hack-around, maybe fix in the future
self.force_login_box_refresh[-1]()
except IndexError:
pass
return
if self.session[SessionStorage].user_id:
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
else:
self.user = None
def build(self) -> Component:
lan_info = self.session[ConfigurationService].get_lan_info()
user_info_and_login_box = UserInfoAndLoginBox()
self.force_login_box_refresh.append(user_info_and_login_box.force_refresh)
user_navigation = [
DesktopNavigationButton("News", "./news"),
Spacer(min_height=1),
DesktopNavigationButton(f"Über {lan_info.name} {lan_info.iteration}", "./overview"),
DesktopNavigationButton("Ticket kaufen", "./buy_ticket"),
DesktopNavigationButton("Sitzplan", "./seating"),
DesktopNavigationButton("Catering", "./catering"),
DesktopNavigationButton("Teilnehmer", "./guests"),
DesktopNavigationButton("Turniere", "./tournaments"),
DesktopNavigationButton("FAQ", "./faq"),
DesktopNavigationButton("Regeln & AGB", "./rules-gtc"),
Spacer(min_height=1),
DesktopNavigationButton("Discord", "#", open_new_tab=True), # Temporarily disabled: https://discord.gg/8gTjg34yyH
DesktopNavigationButton("Die EZ GG e.V.", "https://ezgg-ev.de/about", open_new_tab=True),
DesktopNavigationButton("Kontakt", "./contact"),
DesktopNavigationButton("Impressum & DSGVO", "./imprint"),
Spacer(min_height=1)
]
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)
return Card(
Column(
Text(lan_info.name, align_x=0.5, margin_top=0.3, style=TextStyle(fill=self.session.theme.hud_color, font_size=2.5)),
Text(f"Edition {lan_info.iteration}", align_x=0.5, style=TextStyle(fill=self.session.theme.hud_color, font_size=1.2), margin_top=0.3, margin_bottom=2),
user_info_and_login_box,
*nav_to_use,
align_y=0
),
color=self.session.theme.neutral_color,
min_width=15,
grow_y=True,
corner_radius=(0.5, 0, 0, 0),
margin_right=0.1
)
@@ -1,194 +0,0 @@
from typing import Callable
from rio import Component, Rectangle, Grid, Column, Row, Text, TextStyle, Color
from src.ez_lan_manager.components.SeatingPlanPixels import SeatPixel, WallPixel, InvisiblePixel, TextPixel
from src.ez_lan_manager.types.Seat import Seat
MAX_GRID_WIDTH_PIXELS = 34
MAX_GRID_HEIGHT_PIXELS = 45
class SeatingPlanLegend(Component):
def build(self) -> Component:
return Column(
Text("Legende", style=TextStyle(fill=self.session.theme.neutral_color), justify="center", margin=1),
Row(
Text("L = Luxus Platz", justify="center", style=TextStyle(fill=self.session.theme.neutral_color)),
Text("N = Normaler Platz", justify="center", style=TextStyle(fill=self.session.theme.neutral_color)),
),
Row(
Rectangle(
content=Column(
Text(f"Freier Platz", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.7), align_x=0.5, selectable=False),
Text(f"", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9), align_x=0.5,
selectable=False, overflow="wrap")
),
min_width=1,
min_height=1,
fill=self.session.theme.success_color,
grow_x=False,
grow_y=False,
hover_fill=self.session.theme.success_color,
transition_time=0.4,
ripple=True
),
Rectangle(
content=Column(
Text(f"Belegter Platz", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.7), align_x=0.5, selectable=False),
Text(f"", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9), align_x=0.5,
selectable=False, overflow="wrap")
),
min_width=1,
min_height=1,
fill=self.session.theme.danger_color,
grow_x=False,
grow_y=False,
hover_fill=self.session.theme.danger_color,
transition_time=0.4,
ripple=True
),
Rectangle(
content=Column(
Text(f"Eigener Platz", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.7), align_x=0.5, selectable=False),
Text(f"", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9), align_x=0.5,
selectable=False, overflow="wrap")
),
min_width=1,
min_height=1,
fill=Color.from_hex("800080"),
grow_x=False,
grow_y=False,
hover_fill=Color.from_hex("800080"),
transition_time=0.4,
ripple=True
),
margin=1,
spacing=1
)
)
class SeatingPlan(Component):
seat_clicked_cb: Callable
seating_info: list[Seat]
def get_seat(self, seat_id: str) -> Seat:
return next(filter(lambda seat: seat.seat_id == seat_id, self.seating_info))
"""
This seating plan is for the community center "Bottenhorn"
"""
def build(self) -> Component:
grid = Grid()
# Outlines
for column_id in range(0, MAX_GRID_WIDTH_PIXELS):
grid.add(InvisiblePixel(), row=0, column=column_id)
for y in range(0, 13):
grid.add(WallPixel(), row=y, column=0)
for y in range(13, 19):
grid.add(InvisiblePixel(), row=y, column=0)
for y in range(19, MAX_GRID_HEIGHT_PIXELS):
grid.add(WallPixel(), row=y, column=0)
# Block A
block_a_margin_left = 12
block_a_margin_top = 1
(grid
.add(SeatPixel("A01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A01")), row=block_a_margin_top, column=block_a_margin_left, width=2, height=3)
.add(SeatPixel("A02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A02")), row=block_a_margin_top + 4, column=block_a_margin_left, width=2, height=3)
.add(SeatPixel("A03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A03")), row=block_a_margin_top + 8, column=block_a_margin_left, width=2, height=3)
.add(SeatPixel("A10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A10")), row=block_a_margin_top, column=block_a_margin_left + 3, width=2, height=3)
.add(SeatPixel("A11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A11")), row=block_a_margin_top + 4, column=block_a_margin_left + 3, width=2, height=3)
.add(SeatPixel("A12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A12")), row=block_a_margin_top + 8, column=block_a_margin_left + 3, width=2, height=3)
)
# Block B
block_b_margin_left = 20
block_b_margin_top = 1
(grid
.add(SeatPixel("B01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B01")), row=block_b_margin_top, column=block_b_margin_left, width=2, height=3)
.add(SeatPixel("B02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B02")), row=block_b_margin_top + 4, column=block_b_margin_left, width=2, height=3)
.add(SeatPixel("B03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B03")), row=block_b_margin_top + 8, column=block_b_margin_left, width=2, height=3)
.add(SeatPixel("B10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B10")), row=block_b_margin_top, column=block_b_margin_left + 3, width=2, height=3)
.add(SeatPixel("B11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B11")), row=block_b_margin_top + 4, column=block_b_margin_left + 3, width=2, height=3)
.add(SeatPixel("B12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B12")), row=block_b_margin_top + 8, column=block_b_margin_left + 3, width=2, height=3)
)
# Block C
block_c_margin_left = 28
block_c_margin_top = 1
(grid
.add(SeatPixel("C01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C01")), row=block_c_margin_top, column=block_c_margin_left, width=2, height=3)
.add(SeatPixel("C02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C02")), row=block_c_margin_top + 4, column=block_c_margin_left, width=2, height=3)
.add(SeatPixel("C03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C03")), row=block_c_margin_top + 8, column=block_c_margin_left, width=2, height=3)
.add(SeatPixel("C10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C10")), row=block_c_margin_top, column=block_c_margin_left + 3, width=2, height=3)
.add(SeatPixel("C11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C11")), row=block_c_margin_top + 4, column=block_c_margin_left + 3, width=2, height=3)
.add(SeatPixel("C12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C12")), row=block_c_margin_top + 8, column=block_c_margin_left + 3, width=2, height=3)
)
# Block D
block_d_margin_left = 20
block_d_margin_top = 20
(grid
.add(SeatPixel("D01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D01")), row=block_d_margin_top, column=block_d_margin_left, width=2, height=3)
.add(SeatPixel("D02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D02")), row=block_d_margin_top + 4, column=block_d_margin_left, width=2, height=3)
.add(SeatPixel("D03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D03")), row=block_d_margin_top + 8, column=block_d_margin_left, width=2, height=3)
.add(SeatPixel("D10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D10")), row=block_d_margin_top, column=block_d_margin_left + 3, width=2, height=3)
.add(SeatPixel("D11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D11")), row=block_d_margin_top + 4, column=block_d_margin_left + 3, width=2, height=3)
.add(SeatPixel("D12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D12")), row=block_d_margin_top + 8, column=block_d_margin_left + 3, width=2, height=3)
)
# Block E
block_e_margin_left = 28
block_e_margin_top = 20
(grid
.add(SeatPixel("E01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E01")), row=block_e_margin_top, column=block_e_margin_left, width=2, height=3)
.add(SeatPixel("E02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E02")), row=block_e_margin_top + 4, column=block_e_margin_left, width=2, height=3)
.add(SeatPixel("E03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E03")), row=block_e_margin_top + 8, column=block_e_margin_left, width=2, height=3)
.add(SeatPixel("E10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E10")), row=block_e_margin_top, column=block_e_margin_left + 3, width=2, height=3)
.add(SeatPixel("E11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E11")), row=block_e_margin_top + 4, column=block_e_margin_left + 3, width=2, height=3)
.add(SeatPixel("E12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E12")), row=block_e_margin_top + 8, column=block_e_margin_left + 3, width=2, height=3)
)
# Middle Wall
for y in range(0, 13):
grid.add(WallPixel(), row=y, column=10)
for y in range(19, MAX_GRID_HEIGHT_PIXELS):
grid.add(WallPixel(), row=y, column=10)
# Stage
for x in range(11, MAX_GRID_WIDTH_PIXELS):
grid.add(WallPixel(), row=35, column=x)
grid.add(TextPixel(text="Bühne"), row=36, column=11, width=24, height=9)
# Drinks
grid.add(TextPixel(text="G\ne\nt\nr\nä\nn\nk\ne"), row=21, column=11, width=3, height=11)
# Sleeping
grid.add(TextPixel(icon_name="material/bed"), row=1, column=1, width=4, height=11)
# Toilet
grid.add(TextPixel(icon_name="material/floor", no_outline=True), row=1, column=7, width=3, height=2)
grid.add(TextPixel(icon_name="material/north", no_outline=True), row=3, column=7, width=3, height=2)
grid.add(TextPixel(icon_name="material/wc"), row=5, column=7, width=3, height=2)
# Entry/Helpdesk
grid.add(TextPixel(text="Einlass\n &Orga"), row=19, column=3, width=7, height=5)
# Wall below Entry/Helpdesk
for y in range(24, MAX_GRID_HEIGHT_PIXELS):
grid.add(WallPixel(), row=y, column=3)
# Entry Arrow
grid.add(TextPixel(icon_name="material/east", no_outline=True), row=15, column=1, width=2, height=2)
return Rectangle(
content=grid,
grow_x=True,
grow_y=True,
stroke_color=self.session.theme.neutral_color,
stroke_width=0.1,
fill=self.session.theme.primary_color,
margin=0.5
)
@@ -1,46 +0,0 @@
from typing import Optional, Callable
from rio import Component, Column, Text, TextStyle, Button, Spacer
class SeatingPlanInfoBox(Component):
show: bool
purchase_cb: Callable
is_booking_blocked: bool
seat_id: Optional[str] = None
seat_occupant: Optional[str] = None
seat_price: int = 0
is_blocked: bool = False
def build(self) -> Component:
if not self.show:
return Spacer()
if self.is_blocked:
return Column(Text(f"Sitzplatz gesperrt", margin=1, style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.4), overflow="wrap", justify="center"), min_height=10)
if self.seat_id is None and self.seat_occupant is None:
return Column(Text(f"Sitzplatz auswählen...", margin=1, style=TextStyle(fill=self.session.theme.neutral_color), overflow="wrap", justify="center"), min_height=10)
return Column(
Text(f"Dieser Sitzplatz ({self.seat_id}) ist gebucht von:", margin=1, style=TextStyle(fill=self.session.theme.neutral_color), overflow="wrap", justify="center"),
Text(f"{self.seat_occupant}", margin_bottom=1, style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.4), overflow="wrap", justify="center"),
min_height=10
) if self.seat_id and self.seat_occupant else Column(
Text(f"Dieser Sitzplatz ({self.seat_id}) ist frei", margin=1, style=TextStyle(fill=self.session.theme.neutral_color), overflow="wrap", justify="center"),
Button(
Text(
f"Buchen",
margin=1,
style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.1),
overflow="wrap",
justify="center"
),
shape="rounded",
style="major",
color="secondary",
margin=1,
grow_y=False,
is_sensitive=not self.is_booking_blocked,
on_press=self.purchase_cb
),
min_height=10
)
@@ -1,15 +0,0 @@
import logging
from rio import Component
from src.ez_lan_manager.components.LoginBox import LoginBox
from src.ez_lan_manager.components.UserInfoBox import UserInfoBox
from src.ez_lan_manager.types.SessionStorage import SessionStorage
logger = logging.getLogger(__name__.split(".")[-1])
class UserInfoAndLoginBox(Component):
def build(self) -> Component:
if self.session[SessionStorage].user_id is None:
return LoginBox(status_change_cb=self.force_refresh)
else:
return UserInfoBox(status_change_cb=self.force_refresh)
@@ -1,24 +0,0 @@
from typing import Optional
from rio import URL, GuardEvent
from src.ez_lan_manager.services.UserService import UserService
from src.ez_lan_manager.types.SessionStorage import SessionStorage
# Guards pages against access from users that are NOT logged in
def logged_in_guard(event: GuardEvent) -> Optional[URL]:
if event.session[SessionStorage].user_id is None:
return URL("./")
# Guards pages against access from users that ARE logged in
def not_logged_in_guard(event: GuardEvent) -> Optional[URL]:
if event.session[SessionStorage].user_id is not None:
return URL("./")
# Guards pages against access from users that are NOT logged in and NOT team members
def team_guard(event: GuardEvent) -> Optional[URL]:
user_id = event.session[SessionStorage].user_id
is_team_member = event.session[SessionStorage].is_team_member
if user_id is None or not is_team_member:
return URL("./")
@@ -1,164 +0,0 @@
# USE THIS ON AN EMPTY DATABASE TO GENERATE DEMO DATA
import asyncio
from datetime import date
import sys
from src.ez_lan_manager import init_services
from src.ez_lan_manager.types.CateringMenuItem import CateringMenuItemCategory
from src.ez_lan_manager.types.News import News
DEMO_USERS = [
{ "user_name": "manfred", "user_mail": "manfred@demomail.com", "password_clear_text": "manfred" }, # Gast
{ "user_name": "gustav", "user_mail": "gustav@demomail.com", "password_clear_text": "gustav" }, # Gast + Ticket(NORMAL)
{ "user_name": "jason", "user_mail": "juergen@demomail.com", "password_clear_text": "jason" }, # Gast + Ticket(NORMAL) + Sitzplatz
{ "user_name": "lisa", "user_mail": "lisa@demomail.com", "password_clear_text": "lisa" }, # Teamler
{ "user_name": "thomas", "user_mail": "thomas@demomail.com", "password_clear_text": "thomas" } # Teamler + Admin
]
async def run() -> None:
services = init_services()
await services[3].init_db_pool()
catering_service = services[1]
user_service = services[8]
accounting_service = services[0]
ticket_service = services[7]
seating_service = services[6]
news_service = services[5]
if input("Generate seating table? (y/N): ").lower() == "y":
sys.exit("This part of the script is currently being reworked... :(")
if not input("Generate users? (Y/n): ").lower() == "n":
# MANFRED
manfred = await user_service.create_user(DEMO_USERS[0]["user_name"], DEMO_USERS[0]["user_mail"], DEMO_USERS[0]["password_clear_text"])
# GUSTAV
gustav = await user_service.create_user(DEMO_USERS[1]["user_name"], DEMO_USERS[1]["user_mail"], DEMO_USERS[1]["password_clear_text"])
await accounting_service.add_balance(gustav.user_id, 100000, "DEMO EINZAHLUNG")
await ticket_service.purchase_ticket(gustav.user_id, "NORMAL")
# JASON
jason = await user_service.create_user(DEMO_USERS[2]["user_name"], DEMO_USERS[2]["user_mail"], DEMO_USERS[2]["password_clear_text"])
await accounting_service.add_balance(jason.user_id, 100000, "DEMO EINZAHLUNG")
await ticket_service.purchase_ticket(jason.user_id, "NORMAL")
await seating_service.seat_user(jason.user_id, "D10")
# LISA
lisa = await user_service.create_user(DEMO_USERS[3]["user_name"], DEMO_USERS[3]["user_mail"], DEMO_USERS[3]["password_clear_text"])
await accounting_service.add_balance(lisa.user_id, 100000, "DEMO EINZAHLUNG")
lisa.is_team_member = True
await user_service.update_user(lisa)
# THOMAS
thomas = await user_service.create_user(DEMO_USERS[4]["user_name"], DEMO_USERS[4]["user_mail"], DEMO_USERS[4]["password_clear_text"])
await accounting_service.add_balance(thomas.user_id, 100000, "DEMO EINZAHLUNG")
thomas.is_team_member = True
thomas.is_admin = True
await user_service.update_user(thomas)
if not input("Generate catering menu? (Y/n): ").lower() == "n":
# MAIN_COURSE
await catering_service.add_menu_item("Schnitzel Wiener Art", "mit Pommes", 1050, CateringMenuItemCategory.MAIN_COURSE)
await catering_service.add_menu_item("Jäger Schnitzel mit Champignonrahm Sauce", "mit Pommes", 1150, CateringMenuItemCategory.MAIN_COURSE)
await catering_service.add_menu_item("Tortellini in Käsesauce mit Fleischfüllung", "", 1050, CateringMenuItemCategory.MAIN_COURSE)
await catering_service.add_menu_item("Tortellini in Käsesauce ohne Fleischfüllung", "Vegetarisch", 1050, CateringMenuItemCategory.MAIN_COURSE)
# SNACK
await catering_service.add_menu_item("Käse Schinken Wrap", "", 500, CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Puten Paprika Wrap", "", 700, CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Tomate Mozzarella Wrap", "", 600, CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Portion Pommes", "", 400, CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Rinds-Currywurst", "", 450, CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Rinds-Currywurst mit Pommes", "", 650, CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Nudelsalat", "", 450, CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Nudelsalat mit Bockwurst", "", 600, CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Kartoffelsalat", "", 450, CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Kartoffelsalat mit Bockwurst", "", 600, CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Sandwichtoast - Schinken", "", 180, CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Sandwichtoast - Käse", "", 180, CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Sandwichtoast - Schinken/Käse", "", 210, CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Sandwichtoast - Salami", "", 180, CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Sandwichtoast - Salami/Käse", "", 210, CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Chips - Western Style", "", 130, CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Nachos - Salted", "", 130, CateringMenuItemCategory.SNACK)
# DESSERT
await catering_service.add_menu_item("Panna Cotta mit Erdbeersauce", "", 700, CateringMenuItemCategory.DESSERT)
await catering_service.add_menu_item("Panna Cotta mit Blaubeersauce", "", 700, CateringMenuItemCategory.DESSERT)
await catering_service.add_menu_item("Mousse au Chocolat", "", 700, CateringMenuItemCategory.DESSERT)
# BREAKFAST
await catering_service.add_menu_item("Fruit Loops", "", 150, CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Smacks", "", 150, CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Knuspermüsli", "Schoko", 200, CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Cini Minis", "", 150, CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Brötchen - Schinken", "mit Margarine", 120, CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Brötchen - Käse", "mit Margarine", 120, CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Brötchen - Schinken/Käse", "mit Margarine", 140, CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Brötchen - Salami", "mit Margarine", 120, CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Brötchen - Salami/Käse", "mit Margarine", 140, CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Brötchen - Nutella", "mit Margarine", 120, CateringMenuItemCategory.BREAKFAST)
# BEVERAGE_NON_ALCOHOLIC
await catering_service.add_menu_item("Wasser - Still", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Wasser - Medium", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Wasser - Spritzig", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Coca-Cola", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Coca-Cola Zero", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Fanta", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Sprite", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Spezi", "von Paulaner, 0,5L Flasche", 150, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Red Bull", "", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Energy", "Hausmarke", 150, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
# BEVERAGE_ALCOHOLIC
await catering_service.add_menu_item("Pils", "0,33L Flasche", 190, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
await catering_service.add_menu_item("Radler", "0,33L Flasche", 190, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
await catering_service.add_menu_item("Diesel", "0,33L Flasche", 190, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
await catering_service.add_menu_item("Apfelwein Pur", "0,33L Flasche", 190, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
await catering_service.add_menu_item("Apfelwein Sauer", "0,33L Flasche", 190, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
await catering_service.add_menu_item("Apfelwein Cola", "0,33L Flasche", 190, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
# BEVERAGE_COCKTAIL
await catering_service.add_menu_item("Vodka Energy", "", 400, CateringMenuItemCategory.BEVERAGE_COCKTAIL)
await catering_service.add_menu_item("Vodka O-Saft", "", 400, CateringMenuItemCategory.BEVERAGE_COCKTAIL)
await catering_service.add_menu_item("Whiskey Cola", "mit Bourbon", 400, CateringMenuItemCategory.BEVERAGE_COCKTAIL)
await catering_service.add_menu_item("Jägermeister Energy", "", 400, CateringMenuItemCategory.BEVERAGE_COCKTAIL)
await catering_service.add_menu_item("Sex on the Beach", "", 550, CateringMenuItemCategory.BEVERAGE_COCKTAIL)
await catering_service.add_menu_item("Long Island Ice Tea", "", 550, CateringMenuItemCategory.BEVERAGE_COCKTAIL)
await catering_service.add_menu_item("Caipirinha", "", 550, CateringMenuItemCategory.BEVERAGE_COCKTAIL)
# BEVERAGE_SHOT
await catering_service.add_menu_item("Jägermeister", "", 200, CateringMenuItemCategory.BEVERAGE_SHOT)
await catering_service.add_menu_item("Tequila", "", 200, CateringMenuItemCategory.BEVERAGE_SHOT)
await catering_service.add_menu_item("PfEZzi", "Getunter Pfefferminz-Schnaps", 199, CateringMenuItemCategory.BEVERAGE_SHOT)
# NON_FOOD
await catering_service.add_menu_item("Zigaretten", "Elixyr", 800, CateringMenuItemCategory.NON_FOOD)
await catering_service.add_menu_item("Mentholfilter", "passend für Elixyr", 120, CateringMenuItemCategory.NON_FOOD)
if not input("Generate default new post? (Y/n): ").lower() == "n":
loops = 0
user = None
while loops < 1000:
user = await user_service.get_user(loops)
if user is not None:
break
loops += 1
if user is None:
sys.exit("Database does not contain users! Exiting...")
await news_service.add_news(News(
news_id=None,
title="Der EZ LAN Manager",
subtitle="Eine Software des EZ GG e.V.",
content="Dies ist eine WIP-Version des EZ LAN Managers. Diese Software soll uns helfen in Zukunft die LAN Parties des EZ GG e.V.'s zu organisieren. Wer Fehler findet darf sie behalten. (Oder er meldet sie)",
author=user,
news_date=date.today()
))
if __name__ == "__main__":
with asyncio.Runner() as loop:
loop.run(run())
-69
View File
@@ -1,69 +0,0 @@
from __future__ import annotations
from typing import * # type: ignore
from rio import Component, event, Spacer, Card, Container, Column, Row, TextStyle, Color, Text, PageView
from src.ez_lan_manager import ConfigurationService, DatabaseService
from src.ez_lan_manager.components.DesktopNavigation import DesktopNavigation
class BasePage(Component):
color = "secondary"
corner_radius = (0, 0.5, 0, 0)
@event.periodic(60)
async def check_db_conn(self) -> None:
is_healthy = await self.session[DatabaseService].is_healthy()
if not is_healthy:
self.session.navigate_to("./db-error")
@event.on_window_size_change
async def on_window_size_change(self):
self.force_refresh()
def build(self) -> Component:
content = Card(
PageView(),
color="secondary",
min_width=38,
corner_radius=(0, 0.5, 0, 0)
)
if self.session.window_width > 28:
return Container(
content=Column(
Column(
Row(
Spacer(grow_x=True, grow_y=True),
DesktopNavigation(),
content,
Spacer(grow_x=True, grow_y=True),
grow_y=True
),
Row(
Spacer(grow_x=True, grow_y=False),
Card(
content=Text(f"EZ LAN Manager Version {self.session[ConfigurationService].APP_VERSION} © EZ GG e.V.", align_x=0.5, align_y=0.5, style=TextStyle(fill=self.session.theme.primary_color, font_size=0.5)),
color=self.session.theme.neutral_color,
corner_radius=(0, 0, 0.5, 0.5),
grow_x=False,
grow_y=False,
min_height=1.2,
min_width=53.1,
margin_bottom=3
),
Spacer(grow_x=True, grow_y=False),
grow_y=False
),
margin_top=4
)
),
grow_x=True,
grow_y=True
)
else:
return Text(
"Der EZ LAN Manager wird\nauf mobilen Endgeräten nur\nim Querformat unterstützt.\nBitte drehe dein Gerät.",
align_x=0.5,
align_y=0.5,
style=TextStyle(fill=Color.from_hex("FFFFFF"), font_size=0.8)
)
-26
View File
@@ -1,26 +0,0 @@
from typing import Optional
from rio import Column, Component, event, Spacer
from src.ez_lan_manager import ConfigurationService, UserService
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ez_lan_manager.components.UserEditForm import UserEditForm
from src.ez_lan_manager.types.SessionStorage import SessionStorage
from src.ez_lan_manager.types.User import User
class EditProfilePage(Component):
user: Optional[User] = None
pfp: Optional[bytes] = None
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Profil bearbeiten")
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
self.pfp = await self.session[UserService].get_profile_picture(self.user.user_id)
def build(self) -> Component:
return Column(
MainViewContentBox(UserEditForm(is_own_profile=True)),
Spacer(grow_y=True)
)
@@ -1,32 +0,0 @@
import logging
from rio import Column, Component, event, TextStyle, Text, Spacer
from src.ez_lan_manager import ConfigurationService
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
logger = logging.getLogger(__name__.split(".")[-1])
class ManageTournamentsPage(Component):
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Turnier Verwaltung")
def build(self) -> Component:
return Column(
MainViewContentBox(
Column(
Text(
text="Turnier Verwaltung",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
)
)
),
Spacer()
)
-178
View File
@@ -1,178 +0,0 @@
import logging
from email_validator import validate_email, EmailNotValidError
from rio import Column, Component, event, Text, TextStyle, TextInput, TextInputChangeEvent, Button
from src.ez_lan_manager import ConfigurationService, UserService, MailingService
from src.ez_lan_manager.components.AnimatedText import AnimatedText
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
MINIMUM_PASSWORD_LENGTH = 6
logger = logging.getLogger(__name__.split(".")[-1])
class RegisterPage(Component):
def on_pw_change(self, _: TextInputChangeEvent) -> None:
if not (self.pw_1.text == self.pw_2.text) or len(self.pw_1.text) < MINIMUM_PASSWORD_LENGTH:
self.pw_1.is_valid = False
self.pw_2.is_valid = False
return
self.pw_1.is_valid = True
self.pw_2.is_valid = True
def on_email_changed(self, change_event: TextInputChangeEvent) -> None:
try:
validate_email(change_event.text, check_deliverability=False)
self.email_input.is_valid = True
except EmailNotValidError:
self.email_input.is_valid = False
def on_user_name_input_change(self, _: TextInputChangeEvent) -> None:
current_text = self.user_name_input.text
if len(current_text) > UserService.MAX_USERNAME_LENGTH:
self.user_name_input.text = current_text[:UserService.MAX_USERNAME_LENGTH]
async def on_submit_button_pressed(self) -> None:
self.submit_button.is_loading = True
self.submit_button.force_refresh()
if len(self.user_name_input.text) < 1:
await self.animated_text.display_text(False, "Nutzername darf nicht leer sein!")
self.submit_button.is_loading = False
return
if not (self.pw_1.text == self.pw_2.text):
await self.animated_text.display_text(False, "Passwörter stimmen nicht überein!")
self.submit_button.is_loading = False
return
if len(self.pw_1.text) < MINIMUM_PASSWORD_LENGTH:
await self.animated_text.display_text(False, f"Passwort muss mindestens {MINIMUM_PASSWORD_LENGTH} Zeichen lang sein!")
self.submit_button.is_loading = False
return
if not self.email_input.is_valid or len(self.email_input.text) < 3:
await self.animated_text.display_text(False, "E-Mail Adresse ungültig!")
self.submit_button.is_loading = False
return
user_service = self.session[UserService]
mailing_service = self.session[MailingService]
lan_info = self.session[ConfigurationService].get_lan_info()
if await user_service.get_user(self.email_input.text) is not None or await user_service.get_user(self.user_name_input.text) is not None:
await self.animated_text.display_text(False, "Benutzername oder E-Mail bereits regestriert!")
self.submit_button.is_loading = False
return
try:
new_user = await user_service.create_user(self.user_name_input.text, self.email_input.text, self.pw_1.text)
if not new_user:
raise RuntimeError("User could not be created")
except Exception as e:
logger.error(f"Unknown error during new user registration: {e}")
await self.animated_text.display_text(False, "Es ist ein unbekannter Fehler aufgetreten :(")
self.submit_button.is_loading = False
return
await mailing_service.send_email(
subject="Erfolgreiche Registrierung",
body=f"Hallo {self.user_name_input.text},\n\n"
f"Du hast dich erfolgreich beim EZ-LAN Manager für {lan_info.name} {lan_info.iteration} registriert.\n\n"
f"Wenn du dich nicht registriert hast, kontaktiere bitte unser Team über unsere Homepage.\n\n"
f"Liebe Grüße\nDein {lan_info.name} - Team",
receiver=self.email_input.text
)
self.submit_button.is_loading = False
await self.animated_text.display_text(True, "Erfolgreich registriert!")
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Registrieren")
def build(self) -> Component:
self.user_name_input = TextInput(
label="Benutzername",
text="",
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
on_change=self.on_user_name_input_change
)
self.email_input = TextInput(
label="E-Mail Adresse",
text="",
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
on_change=self.on_email_changed
)
self.pw_1 = TextInput(
label="Passwort",
text="",
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
is_secret=True,
on_change=self.on_pw_change
)
self.pw_2 = TextInput(
label="Passwort wiederholen",
text="",
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
is_secret=True,
on_change=self.on_pw_change
)
self.submit_button = Button(
content=Text(
"Registrieren",
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9),
align_x=0.5
),
grow_x=True,
margin_top=2,
margin_left=1,
margin_right=1,
margin_bottom=1,
shape="rectangle",
style="minor",
color=self.session.theme.secondary_color,
on_press=self.on_submit_button_pressed
)
self.animated_text = AnimatedText(
margin_top=2,
margin_left=1,
margin_right=1,
margin_bottom=2
)
return Column(
MainViewContentBox(
content=Column(
Text(
"Neues Konto anlegen",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
),
self.user_name_input,
self.email_input,
self.pw_1,
self.pw_2,
self.submit_button,
self.animated_text
)
),
align_y=0,
)
@@ -1,38 +0,0 @@
from rio import Column, Component, event, TextStyle, Text
from src.ez_lan_manager import ConfigurationService
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
class TournamentsPage(Component):
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Turniere")
def build(self) -> Component:
return Column(
MainViewContentBox(
Column(
Text(
text="Turniere",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=0,
align_x=0.5
),
Text(
text="Aktuell ist noch kein Turnierplan hinterlegt.",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.9
),
margin=1,
overflow="wrap"
)
)
),
align_y=0
)
@@ -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()
+40
View File
@@ -0,0 +1,40 @@
import logging
from from_root import from_root
from src.ezgg_lan_manager.services import *
from src.ezgg_lan_manager.services.AccountingService import AccountingService
from src.ezgg_lan_manager.services.CateringService import CateringService
from src.ezgg_lan_manager.services.ConfigurationService import ConfigurationService
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
from src.ezgg_lan_manager.services.LocalDataService import LocalDataService
from src.ezgg_lan_manager.services.MailingService import MailingService
from src.ezgg_lan_manager.services.NewsService import NewsService
from src.ezgg_lan_manager.services.RefreshService import RefreshService
from src.ezgg_lan_manager.services.ReceiptPrintingService import ReceiptPrintingService
from src.ezgg_lan_manager.services.SeatingService import SeatingService
from src.ezgg_lan_manager.services.TeamService import TeamService
from src.ezgg_lan_manager.services.TicketingService import TicketingService
from src.ezgg_lan_manager.services.TournamentService import TournamentService
from src.ezgg_lan_manager.services.UserService import UserService
from src.ezgg_lan_manager.types import *
# Inits services in the correct order
def init_services() -> tuple[AccountingService, CateringService, ConfigurationService, DatabaseService, MailingService, NewsService, SeatingService, TicketingService, UserService, LocalDataService, ReceiptPrintingService, TournamentService, TeamService, RefreshService]:
logging.basicConfig(level=logging.DEBUG)
configuration_service = ConfigurationService(from_root("config.toml"))
db_service = DatabaseService(configuration_service.get_database_configuration())
user_service = UserService(db_service)
accounting_service = AccountingService(db_service)
news_service = NewsService(db_service)
mailing_service = MailingService(configuration_service)
ticketing_service = TicketingService(configuration_service.get_ticket_info(), db_service, accounting_service)
seating_service = SeatingService(configuration_service.get_lan_info(), db_service, ticketing_service)
receipt_printing_service = ReceiptPrintingService(seating_service, configuration_service.get_receipt_printing_configuration(), configuration_service.DEV_MODE_ACTIVE)
catering_service = CateringService(db_service, accounting_service, user_service, receipt_printing_service)
local_data_service = LocalDataService()
tournament_service = TournamentService(db_service, user_service)
team_service = TeamService(db_service)
refresh_service = RefreshService()
return accounting_service, catering_service, configuration_service, db_service, mailing_service, news_service, seating_service, ticketing_service, user_service, local_data_service, receipt_printing_service, tournament_service, team_service, refresh_service

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 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,15 +1,16 @@
from typing import Callable
from decimal import Decimal
import rio
from rio import Component, Row, Text, IconButton, TextStyle
from src.ez_lan_manager import AccountingService
from src.ezgg_lan_manager import AccountingService
MAX_LEN = 24
class CateringCartItem(Component):
article_name: str
article_price: int
article_price: Decimal
article_id: int
list_id: int
remove_item_cb: Callable
@@ -24,7 +25,7 @@ class CateringCartItem(Component):
def build(self) -> rio.Component:
return Row(
Text(self.ellipsize_string(self.article_name), align_x=0, overflow="wrap", min_width=19, style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
Text(AccountingService.make_euro_string_from_int(self.article_price), style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
Text(AccountingService.make_euro_string_from_decimal(self.article_price), style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
IconButton(icon="material/close", min_size=2, color=self.session.theme.danger_color, style="plain-text", on_press=lambda: self.remove_item_cb(self.list_id)),
proportions=(19, 5, 2)
)
@@ -3,9 +3,9 @@ from typing import Optional, Callable
from rio import Component, Row, Card, Column, Text, TextStyle, Spacer, PointerEventListener, Button
from src.ez_lan_manager.services.CateringService import CateringService
from src.ez_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus
from src.ez_lan_manager.types.Seat import Seat
from src.ezgg_lan_manager.services.CateringService import CateringService
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus
from src.ezgg_lan_manager.types.Seat import Seat
class CateringManagementOrderDisplayStatusButton(Component):
status: CateringOrderStatus
@@ -65,6 +65,9 @@ class CateringManagementOrderDisplay(Component):
return PointerEventListener(
content=Card(
content=Column(
Row(
Text(f"ID: {self.order.order_id}", margin_left=0.3, margin_top=0.2, justify="center", style=TextStyle(font_size=1.2)),
),
Row(
Text(f"Status: ", margin_left=0.3, margin_top=0.2),
self.format_order_status(self.order.status),
@@ -1,9 +1,8 @@
from typing import Callable
from rio import Component, Row, Text, TextStyle, Color, Rectangle, CursorStyle
from rio.components.pointer_event_listener import PointerEvent, PointerEventListener
from rio import Component, Row, Text, TextStyle, Color, Rectangle, PointerEventListener
from src.ez_lan_manager.types.CateringOrder import CateringOrderStatus, CateringOrder
from src.ezgg_lan_manager.types.CateringOrder import CateringOrderStatus, CateringOrder
MAX_LEN = 24
@@ -41,7 +40,7 @@ class CateringOrderItem(Component):
fill=self.session.theme.primary_color,
hover_fill=self.session.theme.hud_color,
transition_time=0.1,
cursor=CursorStyle.POINTER
cursor="pointer"
),
on_press=lambda _: self.info_modal_cb(self.order),
)
@@ -1,15 +1,17 @@
from decimal import Decimal
from typing import Callable
import rio
from rio import Component, Row, Text, IconButton, TextStyle, Column, Spacer, Card, Color
from src.ez_lan_manager import AccountingService
from src.ezgg_lan_manager import AccountingService
MAX_LEN = 24
class CateringSelectionItem(Component):
article_name: str
article_price: int
article_price: Decimal
article_id: int
on_add_callback: Callable
is_sensitive: bool
@@ -33,27 +35,31 @@ class CateringSelectionItem(Component):
return top.strip(), bottom.strip()
def build(self) -> rio.Component:
article_name_top, article_name_bottom = self.split_article_name(self.article_name)
return Card(
content=Column(
Row(
Text(article_name_top, align_x=0, overflow="wrap", min_width=19, style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
Text(AccountingService.make_euro_string_from_int(self.article_price), style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
Text(article_name_top, align_x=0, overflow="wrap", min_width=19,
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
Text(AccountingService.make_euro_string_from_decimal(self.article_price),
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
IconButton(
icon="material/add",
icon="material/add" if self.is_sensitive else "material/do_not_disturb_on_total_silence",
min_size=2,
color=self.session.theme.success_color,
style="plain-text",
color=self.session.theme.success_color if self.is_sensitive else self.session.theme.danger_color,
style="colored-text",
on_press=lambda: self.on_add_callback(self.article_id),
is_sensitive=self.is_sensitive
),
proportions=(19, 5, 2),
margin_bottom=0
),
Spacer() if not article_name_bottom else Text(article_name_bottom, align_x=0, overflow="wrap", min_width=19, style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
Spacer() if not article_name_bottom else Text(article_name_bottom, align_x=0, overflow="wrap",
min_width=19,
style=TextStyle(fill=self.session.theme.background_color,
font_size=0.9)),
Row(
Text(
self.additional_info,
@@ -0,0 +1,63 @@
from typing import Optional, Callable
from rio import Component, event, Spacer, Card, Column, Text, TextStyle
from src.ezgg_lan_manager.services.ConfigurationService import ConfigurationService
from src.ezgg_lan_manager.services.UserService import UserService
from src.ezgg_lan_manager.components.DesktopNavigationButton import DesktopNavigationButton
from src.ezgg_lan_manager.components.NavigationSponsorBox import NavigationSponsorBox
from src.ezgg_lan_manager.components.UserInfoAndLoginBox import UserInfoAndLoginBox
from src.ezgg_lan_manager.types.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
class DesktopNavigation(Component):
user: Optional[User] = None
@event.on_populate
async def on_populate(self) -> None:
try:
self.user = await self.session[UserService].get_user(self.session[UserSession].user_id)
except KeyError:
self.user = None
def build(self) -> Component:
lan_info = self.session[ConfigurationService].get_lan_info()
user_info_and_login_box = UserInfoAndLoginBox(state_changed_cb=self.on_populate)
navigation = [
DesktopNavigationButton("News", "./news"),
Spacer(min_height=0.7),
DesktopNavigationButton(f"Über {lan_info.name} {lan_info.iteration}", "./overview"),
DesktopNavigationButton("Ticket kaufen", "./buy_ticket"),
DesktopNavigationButton("Sitzplan", "./seating"),
DesktopNavigationButton("Catering", "./catering"),
DesktopNavigationButton("Teilnehmer", "./guests"),
DesktopNavigationButton("Teams", "./teams"),
DesktopNavigationButton("Turniere", "./tournaments"),
DesktopNavigationButton("FAQ", "./faq"),
DesktopNavigationButton("Regeln & AGB", "./rules-gtc"),
Spacer(min_height=0.7),
DesktopNavigationButton("Discord", "https://discord.gg/8gTjg34yyH", open_new_tab=True),
DesktopNavigationButton("Die EZ GG e.V.", "https://ezgg-ev.de/about", open_new_tab=True),
Spacer(min_height=0.7)
]
if self.user is not None and self.user.is_team_member:
navigation.insert(0, DesktopNavigationButton("Adminbereich", "./admin", is_team_navigation=True))
return Card(
Column(
Text(lan_info.name, align_x=0.5, margin_top=0.3, style=TextStyle(fill=self.session.theme.hud_color, font_size=2.5), font_weight="bold"),
Text(f"Edition {lan_info.iteration}", align_x=0.5, style=TextStyle(fill=self.session.theme.hud_color, font_size=1.6), margin_top=0.3, margin_bottom=2, font_weight="bold"),
user_info_and_login_box,
*navigation,
Text("Unsere Sponsoren", align_x=0.5, style=TextStyle(fill=self.session.theme.hud_color, font_size=0.9), margin_bottom=0.5, margin_top=1),
NavigationSponsorBox(img_name="crackz", url="https://www.crackz.gg/"),
align_y=0
),
color=self.session.theme.neutral_color,
min_width=15,
grow_y=True,
corner_radius=(0.5, 0, 0, 0),
margin_right=0.1
)
@@ -1,7 +1,7 @@
from rio import Component, TextStyle, Color, Link, Button, Text
class DesktopNavigationButton(Component):
STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=1.2, font_weight="bold")
TEAM_STYLE = TextStyle(fill=Color.from_hex("F0EADE"), font_size=0.9)
label: str
target_url: str
@@ -1,15 +1,17 @@
from rio import Component, TextStyle, Color, TextInput, Button, Text, Rectangle, Column, Row, Spacer, \
EventHandler
import uuid
from src.ez_lan_manager.services.LocalDataService import LocalDataService, LocalData
from src.ez_lan_manager.services.UserService import UserService
from src.ez_lan_manager.types.SessionStorage import SessionStorage
from src.ez_lan_manager.types.User import User
from rio import Component, TextStyle, Color, TextInput, Button, Text, Rectangle, Column, Row, Spacer, \
EventHandler, Webview
from src.ezgg_lan_manager import RefreshService
from src.ezgg_lan_manager.services.LocalDataService import LocalDataService, LocalData
from src.ezgg_lan_manager.services.UserService import UserService
from src.ezgg_lan_manager.types.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
class LoginBox(Component):
status_change_cb: EventHandler = None
TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
user_name_input_text: str = ""
password_input_text: str = ""
user_name_input_is_valid = True
@@ -27,11 +29,13 @@ class LoginBox(Component):
self.password_input_is_valid = True
self.login_button_is_loading = False
self.is_account_locked = False
await self.session[SessionStorage].set_user_id_and_team_member_flag(user.user_id, user.is_team_member)
token = self.session[LocalDataService].set_session(self.session[SessionStorage])
user_session = UserSession(id=uuid.uuid4(), user_id=user.user_id, is_team_member=user.is_team_member)
self.session.attach(user_session)
token = self.session[LocalDataService].set_session(user_session)
self.session[LocalData].stored_session_token = token
self.session.attach(self.session[LocalData])
self.status_change_cb()
await self.status_change_cb()
await self.session[RefreshService].trigger_refresh()
else:
self.user_name_input_is_valid = False
self.password_input_is_valid = False
@@ -57,7 +61,7 @@ class LoginBox(Component):
is_valid=self.password_input_is_valid
)
login_button = Button(
Text("LOGIN", style=self.TEXT_STYLE, justify="center"),
Text("LOGIN", fill=Color.from_hex("02dac5"), style=TextStyle(font_size=0.9), justify="center"),
shape="rectangle",
style="minor",
color="secondary",
@@ -65,14 +69,14 @@ class LoginBox(Component):
on_press=self._on_login_pressed
)
register_button = Button(
Text("REG", style=self.TEXT_STYLE, justify="center"),
Text("REG", fill=Color.from_hex("02dac5"), style=TextStyle(font_size=0.9), justify="center"),
shape="rectangle",
style="minor",
color="secondary",
on_press=lambda: self.session.navigate_to("./register")
)
forgot_password_button = Button(
Text("LST PWD", style=self.TEXT_STYLE, justify="center"),
Text("LST PWD", fill=Color.from_hex("02dac5"), style=TextStyle(font_size=0.9), justify="center"),
shape="rectangle",
style="minor",
color="secondary",
@@ -95,7 +99,7 @@ class LoginBox(Component):
),
margin_bottom=0.5
),
Text(text="Dieses Konto\nist gesperrt", style=TextStyle(fill=self.session.theme.danger_color, font_size=0.9 if self.is_account_locked else 0), align_x=0.5),
Text(text="Dieses Konto\nist gesperrt", fill=self.session.theme.danger_color, style=TextStyle(font_size=0.9 if self.is_account_locked else 0), align_x=0.5),
spacing=0.4
),
fill=Color.TRANSPARENT,
@@ -103,5 +107,5 @@ class LoginBox(Component):
min_width=12,
align_x=0.5,
margin_top=0.3,
margin_bottom=2
)
margin_bottom=1.5
)
@@ -0,0 +1,23 @@
from from_root import from_root
from rio import Component, Link, Rectangle, Image, Color
class NavigationSponsorBox(Component):
img_name: str
url: str
img_suffix: str = "png"
def build(self) -> Component:
return Link(
content=Rectangle(
content=Image(image=from_root(f"src/ezgg_lan_manager/assets/img/{self.img_name}.{self.img_suffix}"), min_width=10, min_height=10),
stroke_width=0.1,
stroke_color=Color.TRANSPARENT,
hover_stroke_width=0.1,
hover_stroke_color=self.session.theme.secondary_color,
margin=0.6,
cursor="pointer"
),
target_url=self.url,
open_in_new_tab=True
)
@@ -1,10 +1,11 @@
from datetime import datetime
from decimal import Decimal
from typing import Optional
from rio import Component, Column, NumberInput, ThemeContextSwitcher, TextInput, Row, Button, EventHandler
from src.ez_lan_manager.types.Transaction import Transaction
from src.ez_lan_manager.types.User import User
from src.ezgg_lan_manager.types.Transaction import Transaction
from src.ezgg_lan_manager.types.User import User
class NewTransactionForm(Component):
@@ -18,7 +19,7 @@ class NewTransactionForm(Component):
self.new_transaction_cb,
Transaction(
user_id=self.user.user_id,
value=round(self.input_value * 100),
value=Decimal(str(self.input_value)),
is_debit=True,
reference=self.input_reason,
transaction_date=datetime.now()
@@ -30,7 +31,7 @@ class NewTransactionForm(Component):
self.new_transaction_cb,
Transaction(
user_id=self.user.user_id,
value=round(self.input_value * 100),
value=Decimal(str(self.input_value)),
is_debit=False,
reference=self.input_reason,
transaction_date=datetime.now()
@@ -45,7 +46,6 @@ class NewTransactionForm(Component):
label="Betrag",
suffix_text="",
decimals=2,
thousands_separator=".",
margin=1,
margin_bottom=0
),
@@ -21,8 +21,8 @@ class NewsPost(Component):
grow_x=True,
margin=2,
margin_bottom=0,
fill=self.session.theme.background_color,
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.3
),
overflow="ellipsize"
@@ -31,8 +31,8 @@ class NewsPost(Component):
self.date,
margin=2,
align_x=1,
fill=self.session.theme.background_color,
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.6
),
overflow="wrap"
@@ -44,8 +44,8 @@ class NewsPost(Component):
margin=2,
margin_top=0,
margin_bottom=0,
fill=self.session.theme.background_color,
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.8
),
overflow="ellipsize"
@@ -53,9 +53,7 @@ class NewsPost(Component):
Text(
self.text,
margin=2,
style=TextStyle(
fill=self.session.theme.background_color
),
fill=self.session.theme.background_color,
overflow="wrap"
),
Text(
@@ -65,8 +63,8 @@ class NewsPost(Component):
margin=2,
margin_top=0,
margin_bottom=1,
fill=self.session.theme.background_color,
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.5,
italic=True
),
@@ -0,0 +1,274 @@
from typing import Callable
from rio import Component, Rectangle, Grid, Column, Row, Text, TextStyle, Color, PointerEventListener, Spacer
from src.ezgg_lan_manager.components.SeatingPlanPixels import SeatPixel, WallPixel, InvisiblePixel, TextPixel
from src.ezgg_lan_manager.types.Seat import Seat
MAX_GRID_WIDTH_PIXELS = 60
MAX_GRID_HEIGHT_PIXELS = 60
class SeatingPlanLegend(Component):
def build(self) -> Component:
return Column(
Text("Legende", style=TextStyle(fill=self.session.theme.neutral_color), justify="center", margin=1),
Row(
Spacer(),
Rectangle(
content=Text("Normaler Platz", style=TextStyle(fill=self.session.theme.neutral_color, font_size=0.7), margin=0.2, justify="center"),
fill=Color.TRANSPARENT,
stroke_width=0.2,
stroke_color=Color.from_hex("003300"),
min_width=20,
margin_right=1
),
Rectangle(
content=Text("Deluxe Platz", style=TextStyle(fill=self.session.theme.neutral_color, font_size=0.7), margin=0.2, justify="center"),
fill=Color.TRANSPARENT,
stroke_width=0.2,
stroke_color=Color.from_hex("66ff99"),
min_width=20
),
Spacer()
),
Row(
Rectangle(
content=Column(
Text(f"Freier Platz", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.7), align_x=0.5, selectable=False),
Text(f"", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9), align_x=0.5,
selectable=False, overflow="wrap")
),
min_width=1,
min_height=1,
fill=self.session.theme.success_color,
grow_x=False,
grow_y=False,
hover_fill=self.session.theme.success_color,
transition_time=0.4,
ripple=True
),
Rectangle(
content=Column(
Text(f"Belegter Platz", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.7), align_x=0.5, selectable=False),
Text(f"", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9), align_x=0.5,
selectable=False, overflow="wrap")
),
min_width=1,
min_height=1,
fill=self.session.theme.danger_color,
grow_x=False,
grow_y=False,
hover_fill=self.session.theme.danger_color,
transition_time=0.4,
ripple=True
),
Rectangle(
content=Column(
Text(f"Eigener Platz", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.7), align_x=0.5, selectable=False),
Text(f"", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9), align_x=0.5,
selectable=False, overflow="wrap")
),
min_width=1,
min_height=1,
fill=Color.from_hex("800080"),
grow_x=False,
grow_y=False,
hover_fill=Color.from_hex("800080"),
transition_time=0.4,
ripple=True
),
margin=1,
spacing=1
)
)
class SeatingPlan(Component):
seat_clicked_cb: Callable
seating_info: list[Seat]
info_clicked_cb: Callable
def get_seat(self, seat_id: str) -> Seat:
seat = next(filter(lambda seat_: seat_.seat_id == seat_id, self.seating_info), None)
return seat if seat else Seat(seat_id="Z99", is_blocked=True, category="LUXUS", user=None)
"""
This seating plan is for the community center "Donsbach"
"""
def build(self) -> Component:
grid = Grid()
# Outlines
for x in range(0, MAX_GRID_WIDTH_PIXELS):
grid.add(WallPixel(), row=0, column=x)
for y in range(0, MAX_GRID_HEIGHT_PIXELS):
grid.add(WallPixel(), row=y, column=0)
for x in range(0, MAX_GRID_WIDTH_PIXELS):
grid.add(WallPixel(), row=MAX_GRID_HEIGHT_PIXELS, column=x)
for x in range(0, 31):
grid.add(WallPixel(), row=15, column=x)
for x in range(41, MAX_GRID_WIDTH_PIXELS):
grid.add(WallPixel(), row=32, column=x)
for x in range(31, 34):
grid.add(WallPixel(), row=32, column=x)
grid.add(WallPixel(), row=19, column=x)
for x in range(42, MAX_GRID_WIDTH_PIXELS):
grid.add(WallPixel(), row=11, column=x)
grid.add(WallPixel(), row=22, column=x)
for x in range(22, 30):
grid.add(WallPixel(), row=5, column=x)
grid.add(WallPixel(), row=10, column=x)
for y in range(5, 11):
grid.add(WallPixel(), row=y, column=21)
grid.add(WallPixel(), row=y, column=30)
for y in range(40, MAX_GRID_HEIGHT_PIXELS):
grid.add(WallPixel(), row=y, column=30)
for y in range(32, 36):
grid.add(WallPixel(), row=y, column=30)
for y in range(19, 33):
grid.add(WallPixel(), row=y, column=34)
for y in range(16, 20):
grid.add(WallPixel(), row=y, column=30)
for y in range(0, 5):
grid.add(WallPixel(), row=y, column=41)
for y in range(9, 15):
grid.add(WallPixel(), row=y, column=41)
for y in range(19, 33):
grid.add(WallPixel(), row=y, column=41)
# Block A
grid.add(SeatPixel("A01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A01"), seat_orientation="bottom"), row=57, column=1, width=5, height=2)
grid.add(SeatPixel("A02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A02"), seat_orientation="bottom"), row=57, column=6, width=5, height=2)
grid.add(SeatPixel("A03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A03"), seat_orientation="bottom"), row=57, column=11, width=5, height=2)
grid.add(SeatPixel("A04", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A04"), seat_orientation="bottom"), row=57, column=16, width=5, height=2)
grid.add(SeatPixel("A05", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A05"), seat_orientation="bottom"), row=57, column=21, width=5, height=2)
grid.add(SeatPixel("A10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A10"), seat_orientation="top"), row=55, column=1, width=5, height=2)
grid.add(SeatPixel("A11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A11"), seat_orientation="top"), row=55, column=6, width=5, height=2)
grid.add(SeatPixel("A12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A12"), seat_orientation="top"), row=55, column=11, width=5, height=2)
grid.add(SeatPixel("A13", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A13"), seat_orientation="top"), row=55, column=16, width=5, height=2)
grid.add(SeatPixel("A14", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A14"), seat_orientation="top"), row=55, column=21, width=5, height=2)
# Block B
grid.add(SeatPixel("B01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B01"), seat_orientation="bottom"), row=50, column=1, width=3, height=2)
grid.add(SeatPixel("B02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B02"), seat_orientation="bottom"), row=50, column=4, width=3, height=2)
grid.add(SeatPixel("B03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B03"), seat_orientation="bottom"), row=50, column=7, width=3, height=2)
grid.add(SeatPixel("B04", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B04"), seat_orientation="bottom"), row=50, column=10, width=3, height=2)
grid.add(SeatPixel("B05", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B05"), seat_orientation="bottom"), row=50, column=13, width=3, height=2)
grid.add(SeatPixel("B06", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B06"), seat_orientation="bottom"), row=50, column=16, width=3, height=2)
grid.add(SeatPixel("B10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B10"), seat_orientation="top"), row=48, column=1, width=3, height=2)
grid.add(SeatPixel("B11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B11"), seat_orientation="top"), row=48, column=4, width=3, height=2)
grid.add(SeatPixel("B12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B12"), seat_orientation="top"), row=48, column=7, width=3, height=2)
grid.add(SeatPixel("B13", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B13"), seat_orientation="top"), row=48, column=10, width=3, height=2)
grid.add(SeatPixel("B14", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B14"), seat_orientation="top"), row=48, column=13, width=3, height=2)
grid.add(SeatPixel("B15", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B15"), seat_orientation="top"), row=48, column=16, width=3, height=2)
# Block C
grid.add(SeatPixel("C01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C01"), seat_orientation="bottom"), row=43, column=1, width=3, height=2)
grid.add(SeatPixel("C02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C02"), seat_orientation="bottom"), row=43, column=4, width=3, height=2)
grid.add(SeatPixel("C03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C03"), seat_orientation="bottom"), row=43, column=7, width=3, height=2)
grid.add(SeatPixel("C04", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C04"), seat_orientation="bottom"), row=43, column=10, width=3, height=2)
grid.add(SeatPixel("C05", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C05"), seat_orientation="bottom"), row=43, column=13, width=3, height=2)
grid.add(SeatPixel("C06", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C06"), seat_orientation="bottom"), row=43, column=16, width=3, height=2)
grid.add(SeatPixel("C10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C10"), seat_orientation="top"), row=41, column=1, width=3, height=2)
grid.add(SeatPixel("C11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C11"), seat_orientation="top"), row=41, column=4, width=3, height=2)
grid.add(SeatPixel("C12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C12"), seat_orientation="top"), row=41, column=7, width=3, height=2)
grid.add(SeatPixel("C13", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C13"), seat_orientation="top"), row=41, column=10, width=3, height=2)
grid.add(SeatPixel("C14", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C14"), seat_orientation="top"), row=41, column=13, width=3, height=2)
grid.add(SeatPixel("C15", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C15"), seat_orientation="top"), row=41, column=16, width=3, height=2)
# Block D
grid.add(SeatPixel("D01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D01"), seat_orientation="bottom"), row=34, column=1, width=5, height=2)
grid.add(SeatPixel("D02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D02"), seat_orientation="bottom"), row=34, column=6, width=5, height=2)
grid.add(SeatPixel("D03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D03"), seat_orientation="bottom"), row=34, column=11, width=5, height=2)
grid.add(SeatPixel("D04", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D04"), seat_orientation="bottom"), row=34, column=16, width=5, height=2)
grid.add(SeatPixel("D05", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D05"), seat_orientation="bottom"), row=34, column=21, width=5, height=2)
grid.add(SeatPixel("D10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D10"), seat_orientation="top"), row=32, column=1, width=5, height=2)
grid.add(SeatPixel("D11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D11"), seat_orientation="top"), row=32, column=6, width=5, height=2)
grid.add(SeatPixel("D12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D12"), seat_orientation="top"), row=32, column=11, width=5, height=2)
grid.add(SeatPixel("D13", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D13"), seat_orientation="top"), row=32, column=16, width=5, height=2)
grid.add(SeatPixel("D14", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D14"), seat_orientation="top"), row=32, column=21, width=5, height=2)
# Block E
grid.add(SeatPixel("E01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E01"), seat_orientation="bottom"), row=27, column=1, width=5, height=2)
grid.add(SeatPixel("E02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E02"), seat_orientation="bottom"), row=27, column=6, width=5, height=2)
grid.add(SeatPixel("E03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E03"), seat_orientation="bottom"), row=27, column=11, width=5, height=2)
grid.add(SeatPixel("E04", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E04"), seat_orientation="bottom"), row=27, column=16, width=5, height=2)
grid.add(SeatPixel("E05", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E05"), seat_orientation="bottom"), row=27, column=21, width=5, height=2)
grid.add(SeatPixel("E10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E10"), seat_orientation="top"), row=25, column=1, width=5, height=2)
grid.add(SeatPixel("E11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E11"), seat_orientation="top"), row=25, column=6, width=5, height=2)
grid.add(SeatPixel("E12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E12"), seat_orientation="top"), row=25, column=11, width=5, height=2)
grid.add(SeatPixel("E13", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E13"), seat_orientation="top"), row=25, column=16, width=5, height=2)
grid.add(SeatPixel("E14", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E14"), seat_orientation="top"), row=25, column=21, width=5, height=2)
# Stage
grid.add(PointerEventListener(
TextPixel(text="Bühne"),
on_press=lambda _: self.info_clicked_cb("Hier darf ab Freitag 20 Uhr ebenfalls geschlafen werden.")
), row=16, column=1, width=29, height=4)
# Drinks
grid.add(PointerEventListener(
TextPixel(text="G\ne\nt\nr\nä\nn\nk\ne"),
on_press=lambda _: self.info_clicked_cb("Ich mag Bier, B - I - R")
), row=20, column=30, width=4, height=12)
# Main Entrance
grid.add(PointerEventListener(
TextPixel(text="H\na\nl\nl\ne\nn\n\ne\ni\nn\ng\na\nn\ng"),
on_press=lambda _: self.info_clicked_cb("Hallo, ich bin ein Haupteingang")
), row=33, column=56, width=4, height=27)
# Sleeping
grid.add(PointerEventListener(
TextPixel(icon_name="material/bed"),
on_press=lambda _: self.info_clicked_cb("In diesem Raum kann geschlafen werden.\nAchtung: Hier werden nicht alle Teilnehmer Platz finden.")
), row=1, column=1, width=20, height=14)
# Toilet
grid.add(PointerEventListener(
TextPixel(icon_name="material/wc"),
on_press=lambda _: self.info_clicked_cb("Damen Toilette")
), row=1, column=42, width=19, height=10)
grid.add(PointerEventListener(
TextPixel(icon_name="material/wc"),
on_press=lambda _: self.info_clicked_cb("Herren Toilette")
), row=12, column=42, width=19, height=10)
# Entry/Helpdesk
grid.add(PointerEventListener(
TextPixel(text="Einlass\n &Orga"),
on_press=lambda _: self.info_clicked_cb("Für alle Anliegen findest du hier rund um die Uhr jemanden vom Team.")
), row=40, column=22, width=8, height=12)
return Rectangle(
content=grid,
grow_x=True,
grow_y=True,
stroke_color=self.session.theme.neutral_color,
stroke_width=0.1,
fill=self.session.theme.primary_color,
margin=0.5
)
@@ -0,0 +1,90 @@
from decimal import Decimal
from typing import Optional, Callable
from rio import Component, Column, Text, TextStyle, Button, Spacer, event
from src.ezgg_lan_manager import TicketingService
from src.ezgg_lan_manager.types.UserSession import UserSession
class SeatingPlanInfoBox(Component):
show: bool
purchase_cb: Callable
is_booking_blocked: bool
seat_id: Optional[str] = None
seat_occupant: Optional[str] = None
seat_price: Decimal = Decimal("0")
is_blocked: bool = False
has_user_ticket: bool = False
booking_button_text: str = ""
override_text: str = "" # If this is set, all other functionality is disabled and the text is shown
@event.on_populate
async def check_ticket(self) -> None:
try:
user_id = self.session[UserSession].user_id
user_ticket = await self.session[TicketingService].get_user_ticket(user_id)
self.has_user_ticket = not (user_ticket is None)
self.booking_button_text = "Buchen" if self.has_user_ticket else "Ticket kaufen"
self.force_refresh()
except KeyError:
return
async def purchase_clicked(self):
if self.has_user_ticket:
await self.purchase_cb()
else:
self.session.navigate_to("./buy_ticket")
def build(self) -> Component:
try:
user_id = self.session[UserSession].user_id
except KeyError:
user_id = None
if self.override_text:
return Column(Text(self.override_text, margin=1,
style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.4), overflow="wrap",
justify="center"), min_height=10)
if not self.show:
return Spacer()
if self.is_blocked:
return Column(Text(f"Sitzplatz gesperrt", margin=1,
style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.4), overflow="wrap",
justify="center"), min_height=10)
if self.seat_id is None and self.seat_occupant is None:
return Column(
Text(f"Sitzplatz auswählen...", margin=1, style=TextStyle(fill=self.session.theme.neutral_color),
overflow="wrap", justify="center"), min_height=10)
return Column(
Text(f"Dieser Sitzplatz ({self.seat_id}) ist gebucht von:", margin=1,
style=TextStyle(fill=self.session.theme.neutral_color), overflow="wrap", justify="center"),
Text(f"{self.seat_occupant}", margin_bottom=1,
style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.4), overflow="wrap",
justify="center"),
min_height=10
) if self.seat_id and self.seat_occupant else Column(
Text(f"Dieser Sitzplatz ({self.seat_id}) ist frei", margin=1,
style=TextStyle(fill=self.session.theme.neutral_color), overflow="wrap", justify="center"),
Button(
Text(
text=self.booking_button_text,
margin=1,
style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.1),
overflow="wrap",
justify="center"
),
shape="rounded",
style="major",
color="secondary",
margin=1,
grow_y=False,
is_sensitive=not self.is_booking_blocked,
on_press=self.purchase_clicked
) if user_id is not None else Text(f"Du musst eingeloggt sein um einen Sitzplatz zu buchen",
margin=1,
style=TextStyle(fill=self.session.theme.neutral_color),
overflow="wrap", justify="center"),
min_height=10
)
@@ -1,44 +1,62 @@
from functools import partial
from rio import Component, Text, Icon, TextStyle, Rectangle, Spacer, Color, PointerEventListener, Column
from typing import Optional, Callable
from rio import Component, Text, Icon, TextStyle, Rectangle, Spacer, Color, PointerEventListener, Column, Row, PointerEvent, Tooltip
from typing import Optional, Callable, Literal
from src.ez_lan_manager.types.Seat import Seat
from src.ez_lan_manager.types.SessionStorage import SessionStorage
from src.ezgg_lan_manager.types.Seat import Seat
from src.ezgg_lan_manager.types.UserSession import UserSession
class SeatPixel(Component):
seat_id: str
on_press_cb: Callable
seat: Seat
seat_orientation: Literal["top", "bottom"]
def determine_color(self) -> Color:
if self.seat.user is not None and self.seat.user.user_id == self.session[SessionStorage].user_id:
try:
user_id = self.session[UserSession].user_id
except KeyError:
user_id = None
if self.seat.user is not None and self.seat.user.user_id == user_id:
return Color.from_hex("800080")
elif self.seat.is_blocked or self.seat.user is not None:
return self.session.theme.danger_color
return self.session.theme.success_color
def build(self) -> Component:
return PointerEventListener(
content=Rectangle(
content=Column(
Text(f"{self.seat_id}", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.7), align_x=0.5, selectable=False),
Text(f"{self.seat.category[0]}", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9), align_x=0.5, selectable=False, overflow="wrap")
),
min_width=1,
min_height=1,
fill=self.determine_color(),
hover_stroke_width = 0.1,
grow_x=True,
grow_y=True,
hover_fill=self.session.theme.hud_color,
transition_time=0.4,
ripple=True
),
on_press=partial(self.on_press_cb, self.seat_id)
text = Text(f"{self.seat_id}", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9), align_x=0.5, selectable=False)
rec = Rectangle(
content=Row(text),
min_width=1,
min_height=1,
fill=self.determine_color(),
stroke_width=0.1,
hover_stroke_width=0.1,
stroke_color=Color.from_hex("003300") if self.seat.category == "NORMAL" else Color.from_hex("66ff99"),
grow_x=True,
grow_y=True,
hover_fill=self.session.theme.hud_color,
transition_time=0.4,
ripple=True
)
if self.seat.user or self.seat.is_blocked:
return PointerEventListener(
content=Tooltip(
anchor=rec,
tip=self.seat.user.user_name if self.seat.user else "Gesperrt",
position=self.seat_orientation,
),
on_press=partial(self.on_press_cb, self.seat_id),
)
else:
return PointerEventListener(
content=rec,
on_press=partial(self.on_press_cb, self.seat_id),
)
class TextPixel(Component):
text: Optional[str] = None
icon_name: Optional[str] = None
@@ -58,13 +76,14 @@ class TextPixel(Component):
fill=self.session.theme.primary_color,
stroke_width=0.0 if self.no_outline else 0.1,
stroke_color=self.session.theme.neutral_color,
hover_stroke_width = None if self.no_outline else 0.1,
hover_stroke_width=None if self.no_outline else 0.1,
grow_x=True,
grow_y=True,
hover_fill=None,
ripple=True
)
class WallPixel(Component):
def build(self) -> Component:
return Rectangle(
@@ -75,6 +94,7 @@ class WallPixel(Component):
grow_y=True,
)
class DebugPixel(Component):
def build(self) -> Component:
return Rectangle(
@@ -82,14 +102,15 @@ class DebugPixel(Component):
min_width=1,
min_height=1,
fill=self.session.theme.success_color,
hover_stroke_color = self.session.theme.hud_color,
hover_stroke_width = 0.1,
hover_stroke_color=self.session.theme.hud_color,
hover_stroke_width=0.1,
grow_x=True,
grow_y=True,
hover_fill=self.session.theme.secondary_color,
transition_time=0.1
)
class InvisiblePixel(Component):
def build(self) -> Component:
return Rectangle(
@@ -100,4 +121,4 @@ class InvisiblePixel(Component):
hover_stroke_width=0.0,
grow_x=True,
grow_y=True
)
)
@@ -1,14 +1,15 @@
from asyncio import sleep, create_task
from decimal import Decimal
from typing import Optional
import rio
from rio import Component, Column, Text, TextStyle, Button, Row, ScrollContainer, Spacer, Popup, Table
from rio import Component, Column, Text, TextStyle, Button, Row, ScrollContainer, Spacer, Popup, Table, event, Card
from src.ez_lan_manager.components.CateringCartItem import CateringCartItem
from src.ez_lan_manager.components.CateringOrderItem import CateringOrderItem
from src.ez_lan_manager.services.AccountingService import AccountingService
from src.ez_lan_manager.services.CateringService import CateringService, CateringError, CateringErrorType
from src.ez_lan_manager.types.CateringOrder import CateringOrder, CateringMenuItemsWithAmount
from src.ez_lan_manager.types.SessionStorage import SessionStorage
from src.ezgg_lan_manager.components.CateringCartItem import CateringCartItem
from src.ezgg_lan_manager.components.CateringOrderItem import CateringOrderItem
from src.ezgg_lan_manager.services.AccountingService import AccountingService
from src.ezgg_lan_manager.services.CateringService import CateringService, CateringError, CateringErrorType
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringMenuItemsWithAmount
from src.ezgg_lan_manager.types.UserSession import UserSession
POPUP_CLOSE_TIMEOUT_SECONDS = 3
@@ -20,13 +21,23 @@ class ShoppingCartAndOrders(Component):
popup_is_shown: bool = False
popup_is_error: bool = True
@event.periodic(5)
async def periodic_refresh_of_orders(self) -> None:
user_id = self._get_user_id()
if not self.show_cart and not self.popup_is_shown and user_id is not None:
self.orders = await self.session[CateringService].get_orders_for_user(user_id)
async def switch(self) -> None:
self.show_cart = not self.show_cart
self.orders = await self.session[CateringService].get_orders_for_user(self.session[SessionStorage].user_id)
user_id = self._get_user_id()
if user_id is not None:
self.orders = await self.session[CateringService].get_orders_for_user(user_id)
async def on_remove_item(self, list_id: int) -> None:
catering_service = self.session[CateringService]
user_id = self.session[SessionStorage].user_id
user_id = self._get_user_id()
if user_id is None:
return
cart = catering_service.get_cart(user_id)
try:
cart.pop(list_id)
@@ -36,13 +47,16 @@ class ShoppingCartAndOrders(Component):
self.force_refresh()
async def on_empty_cart_pressed(self) -> None:
self.session[CateringService].save_cart(self.session[SessionStorage].user_id, [])
user_id = self._get_user_id()
if user_id is None:
return
self.session[CateringService].save_cart(user_id, [])
self.force_refresh()
async def on_add_item(self, article_id: int) -> None:
catering_service = self.session[CateringService]
user_id = self.session[SessionStorage].user_id
if not user_id:
user_id = self._get_user_id()
if user_id is None:
return
cart = catering_service.get_cart(user_id)
item_to_add = await catering_service.get_menu_item_by_id(article_id)
@@ -63,7 +77,9 @@ class ShoppingCartAndOrders(Component):
self.order_button_loading = True
self.force_refresh()
user_id = self.session[SessionStorage].user_id
user_id = self._get_user_id()
if user_id is None:
return
cart = self.session[CateringService].get_cart(user_id)
show_popup_task = None
if len(cart) < 1:
@@ -84,25 +100,26 @@ class ShoppingCartAndOrders(Component):
show_popup_task = create_task(self.show_popup("Guthaben nicht ausreichend", True))
else:
show_popup_task = create_task(self.show_popup("Unbekannter Fehler", True))
self.session[CateringService].save_cart(self.session[SessionStorage].user_id, [])
else:
self.session[CateringService].save_cart(user_id, [])
self.order_button_loading = False
if not show_popup_task:
show_popup_task = create_task(self.show_popup("Bestellung erfolgreich aufgegeben!", False))
async def _create_order_info_modal(self, order: CateringOrder) -> None:
def build_dialog_content() -> rio.Component:
def build_dialog_content() -> Component:
# @todo: rio 0.10.8 did not have the ability to align the columns, check back in a future version
table = Table(
{
"Artikel": [item.name for item in order.items.keys()] + ["Gesamtpreis:"],
"Anzahl": [item for item in order.items.values()] + [""],
"Preis": [AccountingService.make_euro_string_from_int(item.price) for item in order.items.keys()] + [AccountingService.make_euro_string_from_int(order.price)],
"Preis": [AccountingService.make_euro_string_from_decimal(item.price) for item in order.items.keys()] + [AccountingService.make_euro_string_from_decimal(order.price)],
},
show_row_numbers=False
)
return rio.Card(
rio.Column(
rio.Text(
return Card(
Column(
Text(
f"Deine Bestellung ({order.order_id})",
align_x=0.5,
margin_bottom=0.5
@@ -123,14 +140,20 @@ class ShoppingCartAndOrders(Component):
dialog = await self.session.show_custom_dialog(
build=build_dialog_content,
modal=True,
user_closeable=True,
user_closable=True,
)
await dialog.wait_for_close()
def build(self) -> rio.Component:
user_id = self.session[SessionStorage].user_id
def _get_user_id(self) -> Optional[int]:
try:
return self.session[UserSession].user_id
except KeyError:
return None
def build(self) -> Component:
user_id = self._get_user_id()
catering_service = self.session[CateringService]
cart = catering_service.get_cart(user_id)
cart = catering_service.get_cart(user_id) if user_id is not None else []
if self.show_cart:
cart_container = ScrollContainer(
content=Column(
@@ -148,7 +171,6 @@ class ShoppingCartAndOrders(Component):
margin=1
)
return Column(
cart_container,
Popup(
anchor=cart_container,
content=Text(self.popup_message, style=TextStyle(fill=self.session.theme.danger_color if self.popup_is_error else self.session.theme.success_color), overflow="wrap", margin=2, justify="center", min_width=20),
@@ -158,7 +180,7 @@ class ShoppingCartAndOrders(Component):
),
Row(
Text(
text=f"Preis: {AccountingService.make_euro_string_from_int(sum(cart_item.price for cart_item in cart))}",
text=f"Preis: {AccountingService.make_euro_string_from_decimal(sum((cart_item.price for cart_item in cart), Decimal(0)))}",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.8
@@ -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
)
@@ -1,18 +1,18 @@
from functools import partial
from typing import Callable, Optional
from decimal import Decimal
import rio
from rio import Component, Card, Column, Text, Row, Button, TextStyle, ProgressBar, event, Spacer
from src.ez_lan_manager import TicketingService
from src.ez_lan_manager.services.AccountingService import AccountingService
from src.ez_lan_manager.types.Ticket import Ticket
from src.ezgg_lan_manager import TicketingService
from src.ezgg_lan_manager.services.AccountingService import AccountingService
from src.ezgg_lan_manager.types.Ticket import Ticket
class TicketBuyCard(Component):
description: str
additional_info: str
price: int
price: Decimal
category: str
pressed_cb: Callable
is_enabled: bool
@@ -21,10 +21,10 @@ class TicketBuyCard(Component):
available_tickets: int = 0
@event.on_populate
async def async_init(self) -> None:
async def on_populate(self) -> None:
self.available_tickets = await self.session[TicketingService].get_available_tickets_for_category(self.category)
def build(self) -> rio.Component:
def build(self) -> Component:
ticket_description_style = TextStyle(
fill=self.session.theme.neutral_color,
font_size=1.2,
@@ -67,7 +67,7 @@ class TicketBuyCard(Component):
margin_right=1
),
Row(
Text(f"{AccountingService.make_euro_string_from_int(self.price)}", margin_left=1, margin_top=1, grow_x=True),
Text(f"{AccountingService.make_euro_string_from_decimal(self.price)}", margin_left=1, margin_top=1, grow_x=True),
Button(
Text("Kaufen", align_x=0.5, margin=0.4),
margin_right=1,
@@ -0,0 +1,35 @@
from typing import Optional
from rio import Component, Row, Text, TextStyle, Color
class TournamentDetailsInfoRow(Component):
key: str
value: str
key_color: Optional[Color] = None
value_color: Optional[Color] = None
def build(self) -> Component:
return Row(
Text(
text=self.key,
style=TextStyle(
fill=self.key_color if self.key_color is not None else self.session.theme.background_color,
font_size=1
),
margin_bottom=0.5,
align_x=0
),
Text(
text=self.value,
style=TextStyle(
fill=self.value_color if self.value_color is not None else self.session.theme.background_color,
font_size=1
),
margin_bottom=0.5,
align_x=1
),
margin_left=4,
margin_right=4
)
@@ -0,0 +1,60 @@
from typing import Literal, Callable
from rio import Component, PointerEventListener, Rectangle, Image, Text, Tooltip, TextStyle, Color, Icon, Row, PointerEvent
from from_root import from_root
from src.ezgg_lan_manager.types.TournamentBase import TournamentStatus
class TournamentPageRow(Component):
tournament_id: int
tournament_name: str
game_image_name: str
current_participants: int
max_participants: int
tournament_status: TournamentStatus
clicked_cb: Callable
def handle_click(self, _: PointerEvent) -> None:
self.clicked_cb(self.tournament_id)
def determine_tournament_status_icon_color_and_text(self) -> tuple[str, Literal["success", "warning", "danger"], str]:
if self.tournament_status == TournamentStatus.OPEN:
return "material/lock_open", "success", "Anmeldung geöffnet"
elif self.tournament_status == TournamentStatus.CLOSED:
return "material/lock", "danger", "Anmeldung geschlossen"
elif self.tournament_status == TournamentStatus.ONGOING:
return "material/autoplay", "warning", "Turnier läuft"
elif self.tournament_status == TournamentStatus.COMPLETED:
return "material/check_circle", "success", "Turnier beendet"
elif self.tournament_status == TournamentStatus.CANCELED:
return "material/cancel", "danger", "Turnier abgesagt"
elif self.tournament_status == TournamentStatus.INVITE_ONLY:
return "material/person_cancel", "warning", "Teilnahme nur per Einladung"
else:
raise RuntimeError(f"Unknown tournament status: {str(self.tournament_status)}")
def build(self) -> Component:
icon_name, color, text = self.determine_tournament_status_icon_color_and_text()
return PointerEventListener(
content=Rectangle(
content=Row(
Image(image=from_root(f"src/ezgg_lan_manager/assets/img/games/{self.game_image_name}")),
Text(self.tournament_name, style=TextStyle(fill=self.session.theme.background_color, font_size=1)),
Text(f"{self.current_participants}/{self.max_participants}", style=TextStyle(fill=self.session.theme.background_color, font_size=1), justify="right", margin_right=0.5),
Tooltip(anchor=Icon(icon_name, min_width=1, min_height=1, fill=color), position="top",
tip=Text(text, style=TextStyle(fill=self.session.theme.background_color, font_size=0.7))),
proportions=[1, 4, 1, 1],
margin=.5
),
fill=self.session.theme.hud_color,
margin=1,
margin_bottom=0,
stroke_color=Color.TRANSPARENT,
stroke_width=0.2,
hover_stroke_color=self.session.theme.background_color,
cursor="pointer"
),
on_press=self.handle_click
)
@@ -7,10 +7,10 @@ from from_root import from_root
from rio import Component, Column, Button, Color, TextStyle, Text, TextInput, Row, Image, event, Spacer, DateInput, \
TextInputChangeEvent, NoFileSelectedError
from src.ez_lan_manager.services.UserService import UserService, NameNotAllowedError
from src.ez_lan_manager.services.ConfigurationService import ConfigurationService
from src.ez_lan_manager.types.SessionStorage import SessionStorage
from src.ez_lan_manager.types.User import User
from src.ezgg_lan_manager.services.UserService import UserService, NameNotAllowedError
from src.ezgg_lan_manager.services.ConfigurationService import ConfigurationService
from src.ezgg_lan_manager.types.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
class UserEditForm(Component):
@@ -35,8 +35,13 @@ class UserEditForm(Component):
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Profil bearbeiten")
if self.is_own_profile:
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
self.profile_picture = await self.session[UserService].get_profile_picture(self.user.user_id)
try:
user_id = self.session[UserSession].user_id
except KeyError:
self.session.navigate_to("/")
else:
self.user = await self.session[UserService].get_user(user_id)
self.profile_picture = await self.session[UserService].get_profile_picture(self.user.user_id)
else:
self.profile_picture = await self.session[UserService].get_profile_picture(self.user.user_id)
@@ -122,7 +127,7 @@ class UserEditForm(Component):
def build(self) -> Component:
pfp_image_container = Image(
from_root("src/ez_lan_manager/assets/img/anon_pfp.png") if self.profile_picture is None else self.profile_picture,
from_root("src/ezgg_lan_manager/assets/img/anon_pfp.png") if self.profile_picture is None else self.profile_picture,
align_x=0.5,
min_width=10,
min_height=10,
@@ -0,0 +1,18 @@
import logging
from typing import Callable
from rio import Component
from src.ezgg_lan_manager.components.LoginBox import LoginBox
from src.ezgg_lan_manager.components.UserInfoBox import UserInfoBox
from src.ezgg_lan_manager.types.UserSession import UserSession
logger = logging.getLogger(__name__.split(".")[-1])
class UserInfoAndLoginBox(Component):
state_changed_cb: Callable
def build(self) -> Component:
try:
user_id = self.session[UserSession].user_id
return UserInfoBox(status_change_cb=self.state_changed_cb, user_id=user_id)
except KeyError:
return LoginBox(status_change_cb=self.state_changed_cb)
@@ -1,18 +1,20 @@
from random import choice
from typing import Optional
from decimal import Decimal
from rio import Component, TextStyle, Color, Button, Text, Rectangle, Column, Row, Spacer, Link, event, EventHandler
from src.ez_lan_manager.components.UserInfoBoxButton import UserInfoBoxButton
from src.ez_lan_manager.services.LocalDataService import LocalData, LocalDataService
from src.ez_lan_manager.services.UserService import UserService
from src.ez_lan_manager.services.AccountingService import AccountingService
from src.ez_lan_manager.services.TicketingService import TicketingService
from src.ez_lan_manager.services.SeatingService import SeatingService
from src.ez_lan_manager.types.Seat import Seat
from src.ez_lan_manager.types.Ticket import Ticket
from src.ez_lan_manager.types.User import User
from src.ez_lan_manager.types.SessionStorage import SessionStorage
from src.ezgg_lan_manager.components.UserInfoBoxButton import UserInfoBoxButton
from src.ezgg_lan_manager.services.LocalDataService import LocalData, LocalDataService
from src.ezgg_lan_manager.services.RefreshService import RefreshService
from src.ezgg_lan_manager.services.UserService import UserService
from src.ezgg_lan_manager.services.AccountingService import AccountingService
from src.ezgg_lan_manager.services.TicketingService import TicketingService
from src.ezgg_lan_manager.services.SeatingService import SeatingService
from src.ezgg_lan_manager.types.Seat import Seat
from src.ezgg_lan_manager.types.Ticket import Ticket
from src.ezgg_lan_manager.types.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
class StatusButton(Component):
@@ -23,26 +25,28 @@ class StatusButton(Component):
def build(self) -> Component:
return Link(
content=Button(
content=Text(self.label, style=self.STYLE, justify="center"),
shape="rectangle",
style="major",
color="success" if self.enabled else "danger",
grow_x=True,
margin_left=0.6,
margin_right=0.6,
margin_top=0.6
content=Button(
content=Text(self.label, style=self.STYLE, justify="center"),
shape="rectangle",
style="major",
color="success" if self.enabled else "danger",
grow_x=True,
margin_left=0.6,
margin_right=0.6,
margin_top=0.6
),
target_url=self.target_url,
align_y=0.5,
grow_y=False
)
class UserInfoBox(Component):
user_id: int
status_change_cb: EventHandler = None
TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
user: Optional[User] = None
user_balance: Optional[int] = 0
user_balance: Optional[Decimal] = Decimal("0")
user_ticket: Optional[Ticket] = None
user_seat: Optional[Seat] = None
@@ -51,37 +55,36 @@ class UserInfoBox(Component):
return choice(["Guten Tacho", "Tuten Gag", "Servus", "Moinjour", "Hallöchen", "Heyho", "Moinsen"])
async def logout(self) -> None:
await self.session[SessionStorage].clear()
self.session.detach(UserSession)
self.user = None
self.session[LocalDataService].del_session(self.session[LocalData].stored_session_token)
self.session[LocalData].stored_session_token = None
self.session.attach(self.session[LocalData])
self.status_change_cb()
self.session.navigate_to("/")
await self.status_change_cb()
await self.session[RefreshService].trigger_refresh()
@event.on_populate
async def async_init(self) -> None:
if self.session[SessionStorage].user_id:
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
self.user_balance = await self.session[AccountingService].get_balance(self.user.user_id)
self.user_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id)
self.user_seat = await self.session[SeatingService].get_user_seat(self.user.user_id)
self.session[AccountingService].add_update_hook(self.update)
async def update(self) -> None:
if not self.user:
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
self.user = await self.session[UserService].get_user(self.user_id)
self.user_balance = await self.session[AccountingService].get_balance(self.user.user_id)
self.user_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id)
self.user_seat = await self.session[SeatingService].get_user_seat(self.user.user_id)
self.session[AccountingService].add_update_hook(self.update)
async def update(self) -> None:
self.user_balance = await self.session[AccountingService].get_balance(self.user_id)
self.user_ticket = await self.session[TicketingService].get_user_ticket(self.user_id)
self.user_seat = await self.session[SeatingService].get_user_seat(self.user_id)
def build(self) -> Component:
if not self.user:
return Spacer()
return Rectangle(
content=Column(
Text(f"{self.get_greeting()},", style=TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9), justify="center"),
Text(f"{self.user.user_name}", style=TextStyle(fill=Color.from_hex("02dac5"), font_size=1.2), justify="center"),
Text(f"{self.get_greeting()},", style=TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9),
justify="center"),
Text(f"{self.user.user_name}", style=TextStyle(fill=Color.from_hex("02dac5"), font_size=1.2),
justify="center"),
Row(
StatusButton(label="TICKET", target_url="./buy_ticket",
enabled=self.user_ticket is not None),
@@ -91,7 +94,9 @@ class UserInfoBox(Component):
grow_y=False
),
UserInfoBoxButton("Profil bearbeiten", "./edit-profile"),
UserInfoBoxButton(f"Guthaben: {self.session[AccountingService].make_euro_string_from_int(self.user_balance)}", "./account"),
UserInfoBoxButton(
f"Guthaben: {self.session[AccountingService].make_euro_string_from_decimal(self.user_balance)}",
"./account"),
Button(
content=Text("Ausloggen", style=TextStyle(fill=Color.from_hex("02dac5"), font_size=0.6)),
shape="rectangle",
@@ -109,5 +114,5 @@ class UserInfoBox(Component):
min_width=12,
align_x=0.5,
margin_top=0.3,
margin_bottom=2
margin_bottom=1.5
)
@@ -0,0 +1,34 @@
from typing import Optional
from rio import URL, GuardEvent
from src.ezgg_lan_manager.services.UserService import UserService
from src.ezgg_lan_manager.types.UserSession import UserSession
# Guards pages against access from users that are NOT logged in
def logged_in_guard(event: GuardEvent) -> Optional[URL]:
try:
_ = event.session[UserSession].user_id
return None
except KeyError:
return URL("./")
# Guards pages against access from users that ARE logged in
def not_logged_in_guard(event: GuardEvent) -> Optional[URL]:
try:
_ = event.session[UserSession].user_id
return URL("./")
except KeyError:
return None
# Guards pages against access from users that are NOT logged in and NOT team members
def team_guard(event: GuardEvent) -> Optional[URL]:
try:
user_id = event.session[UserSession].user_id
is_team_member = event.session[UserSession].is_team_member
if user_id and is_team_member:
return None
return URL("./")
except KeyError:
return URL("./")
@@ -0,0 +1,200 @@
# USE THIS ON AN EMPTY DATABASE TO GENERATE DEMO DATA
import asyncio
from datetime import date
from decimal import Decimal
import sys
from src.ezgg_lan_manager import init_services
from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItemCategory
from src.ezgg_lan_manager.types.News import News
DEMO_USERS = [
{"user_name": "manfred", "user_mail": "manfred@demomail.com", "password_clear_text": "manfred"}, # Gast
{"user_name": "gustav", "user_mail": "gustav@demomail.com", "password_clear_text": "gustav"}, # Gast + Ticket(NORMAL)
{"user_name": "jason", "user_mail": "juergen@demomail.com", "password_clear_text": "jason"}, # Gast + Ticket(NORMAL) + Sitzplatz
{"user_name": "lisa", "user_mail": "lisa@demomail.com", "password_clear_text": "lisa"}, # Teamler
{"user_name": "thomas", "user_mail": "thomas@demomail.com", "password_clear_text": "thomas"} # Teamler + Admin
]
async def run() -> None:
services = init_services()
await services[3].init_db_pool()
catering_service = services[1]
user_service = services[8]
accounting_service = services[0]
ticket_service = services[7]
seating_service = services[6]
news_service = services[5]
if input("Generate seating table? (y/N): ").lower() == "y":
sys.exit("This part of the script is currently being reworked... :(")
if not input("Generate users? (Y/n): ").lower() == "n":
# MANFRED
manfred = await user_service.create_user(DEMO_USERS[0]["user_name"], DEMO_USERS[0]["user_mail"],
DEMO_USERS[0]["password_clear_text"])
# GUSTAV
gustav = await user_service.create_user(DEMO_USERS[1]["user_name"], DEMO_USERS[1]["user_mail"],
DEMO_USERS[1]["password_clear_text"])
await accounting_service.add_balance(gustav.user_id, Decimal("1000.00"), "DEMO EINZAHLUNG")
await ticket_service.purchase_ticket(gustav.user_id, "NORMAL")
# JASON
jason = await user_service.create_user(DEMO_USERS[2]["user_name"], DEMO_USERS[2]["user_mail"],
DEMO_USERS[2]["password_clear_text"])
await accounting_service.add_balance(jason.user_id, Decimal("1000.00"), "DEMO EINZAHLUNG")
await ticket_service.purchase_ticket(jason.user_id, "NORMAL")
# LISA
lisa = await user_service.create_user(DEMO_USERS[3]["user_name"], DEMO_USERS[3]["user_mail"],
DEMO_USERS[3]["password_clear_text"])
await accounting_service.add_balance(lisa.user_id, Decimal("1000.00"), "DEMO EINZAHLUNG")
lisa.is_team_member = True
await user_service.update_user(lisa)
# THOMAS
thomas = await user_service.create_user(DEMO_USERS[4]["user_name"], DEMO_USERS[4]["user_mail"],
DEMO_USERS[4]["password_clear_text"])
await accounting_service.add_balance(thomas.user_id, Decimal("1000.00"), "DEMO EINZAHLUNG")
thomas.is_team_member = True
thomas.is_admin = True
await user_service.update_user(thomas)
if not input("Generate catering menu? (Y/n): ").lower() == "n":
# MAIN_COURSE
await catering_service.add_menu_item("Schnitzel Wiener Art", "mit Pommes", Decimal("10.00"),
CateringMenuItemCategory.MAIN_COURSE)
await catering_service.add_menu_item("Jäger Schnitzel mit Champignonrahm Sauce", "mit Pommes", Decimal("11.50"),
CateringMenuItemCategory.MAIN_COURSE)
await catering_service.add_menu_item("Tortellini in Käsesauce mit Fleischfüllung", "", Decimal("10.50"),
CateringMenuItemCategory.MAIN_COURSE)
await catering_service.add_menu_item("Tortellini in Käsesauce ohne Fleischfüllung", "Vegetarisch", Decimal("10.50"),
CateringMenuItemCategory.MAIN_COURSE)
# SNACK
await catering_service.add_menu_item("Käse Schinken Wrap", "", Decimal("5.00"), CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Puten Paprika Wrap", "", Decimal("7.00"), CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Tomate Mozzarella Wrap", "", Decimal("6.00"), CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Portion Pommes", "", Decimal("4.00"), CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Rinds-Currywurst", "", Decimal("4.50"), CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Rinds-Currywurst mit Pommes", "", Decimal("6.50"), CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Nudelsalat", "", Decimal("4.50"), CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Nudelsalat mit Bockwurst", "", Decimal("6.00"), CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Kartoffelsalat", "", Decimal("4.50"), CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Kartoffelsalat mit Bockwurst", "", Decimal("6.00"), CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Sandwichtoast - Schinken", "", Decimal("1.80"), CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Sandwichtoast - Käse", "", Decimal("1.80"), CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Sandwichtoast - Schinken/Käse", "", Decimal("2.10"), CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Sandwichtoast - Salami", "", Decimal("1.80"), CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Sandwichtoast - Salami/Käse", "", Decimal("2.10"), CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Chips - Western Style", "", Decimal("1.30"), CateringMenuItemCategory.SNACK)
await catering_service.add_menu_item("Nachos - Salted", "", Decimal("1.30"), CateringMenuItemCategory.SNACK)
# DESSERT
await catering_service.add_menu_item("Panna Cotta mit Erdbeersauce", "", Decimal("7.00"), CateringMenuItemCategory.DESSERT)
await catering_service.add_menu_item("Panna Cotta mit Blaubeersauce", "", Decimal("7.00"), CateringMenuItemCategory.DESSERT)
await catering_service.add_menu_item("Mousse au Chocolat", "", Decimal("7.00"), CateringMenuItemCategory.DESSERT)
# BREAKFAST
await catering_service.add_menu_item("Fruit Loops", "", Decimal("1.50"), CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Smacks", "", Decimal("1.50"), CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Knuspermüsli", "Schoko", Decimal("2.00"), CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Cini Minis", "", Decimal("2.50"), CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Brötchen - Schinken", "mit Margarine", Decimal("1.20"),
CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Brötchen - Käse", "mit Margarine", Decimal("1.20"),
CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Brötchen - Schinken/Käse", "mit Margarine", Decimal("1.40"),
CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Brötchen - Salami", "mit Margarine", Decimal("1.20"),
CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Brötchen - Salami/Käse", "mit Margarine", Decimal("1.40"),
CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Brötchen - Nutella", "mit Margarine", Decimal("1.20"),
CateringMenuItemCategory.BREAKFAST)
# BEVERAGE_NON_ALCOHOLIC
await catering_service.add_menu_item("Wasser - Still", "1L Flasche", Decimal("2.00"),
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Wasser - Medium", "1L Flasche", Decimal("2.00"),
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Wasser - Spritzig", "1L Flasche", Decimal("2.00"),
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Coca-Cola", "1L Flasche", Decimal("2.00"),
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Coca-Cola Zero", "1L Flasche", Decimal("2.00"),
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Fanta", "1L Flasche", Decimal("2.00"),
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Sprite", "1L Flasche", Decimal("2.00"),
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Spezi", "von Paulaner, 0,5L Flasche", Decimal("1.50"),
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Red Bull", "", Decimal("2.00"), CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Energy", "Hausmarke", Decimal("1.50"),
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
# BEVERAGE_ALCOHOLIC
await catering_service.add_menu_item("Pils", "0,33L Flasche", Decimal("1.90"), CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
await catering_service.add_menu_item("Radler", "0,33L Flasche", Decimal("1.90"),
CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
await catering_service.add_menu_item("Diesel", "0,33L Flasche", Decimal("1.90"),
CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
await catering_service.add_menu_item("Apfelwein Pur", "0,33L Flasche", Decimal("1.90"),
CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
await catering_service.add_menu_item("Apfelwein Sauer", "0,33L Flasche", Decimal("1.90"),
CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
await catering_service.add_menu_item("Apfelwein Cola", "0,33L Flasche", Decimal("1.90"),
CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
# BEVERAGE_COCKTAIL
await catering_service.add_menu_item("Vodka Energy", "", Decimal("4.00"), CateringMenuItemCategory.BEVERAGE_COCKTAIL)
await catering_service.add_menu_item("Vodka O-Saft", "", Decimal("4.00"), CateringMenuItemCategory.BEVERAGE_COCKTAIL)
await catering_service.add_menu_item("Whiskey Cola", "mit Bourbon", Decimal("4.00"),
CateringMenuItemCategory.BEVERAGE_COCKTAIL)
await catering_service.add_menu_item("Jägermeister Energy", "", Decimal("4.00"), CateringMenuItemCategory.BEVERAGE_COCKTAIL)
await catering_service.add_menu_item("Sex on the Beach", "", Decimal("5.50"), CateringMenuItemCategory.BEVERAGE_COCKTAIL)
await catering_service.add_menu_item("Long Island Ice Tea", "", Decimal("5.50"), CateringMenuItemCategory.BEVERAGE_COCKTAIL)
await catering_service.add_menu_item("Caipirinha", "", Decimal("5.50"), CateringMenuItemCategory.BEVERAGE_COCKTAIL)
# BEVERAGE_SHOT
await catering_service.add_menu_item("Jägermeister", "", Decimal("2.00"), CateringMenuItemCategory.BEVERAGE_SHOT)
await catering_service.add_menu_item("Tequila", "", Decimal("2.00"), CateringMenuItemCategory.BEVERAGE_SHOT)
await catering_service.add_menu_item("PfEZzi", "Getunter Pfefferminz-Schnaps", Decimal("1.99"),
CateringMenuItemCategory.BEVERAGE_SHOT)
# NON_FOOD
await catering_service.add_menu_item("Zigaretten", "Elixyr", Decimal("8.00"), CateringMenuItemCategory.NON_FOOD)
await catering_service.add_menu_item("Mentholfilter", "passend für Elixyr", Decimal("1.20"),
CateringMenuItemCategory.NON_FOOD)
if not input("Generate default new post? (Y/n): ").lower() == "n":
loops = 0
user = None
while loops < 1000:
user = await user_service.get_user(loops)
if user is not None:
break
loops += 1
if user is None:
sys.exit("Database does not contain users! Exiting...")
await news_service.add_news(News(
news_id=None,
title="Der EZGG LAN Manager",
subtitle="Eine Software des EZ GG e.V.",
content="Dies ist eine WIP-Version des EZGG LAN Managers. Diese Software soll uns helfen in Zukunft die LAN "
"Parties des EZ GG e.V.'s zu organisieren. Wer Fehler findet darf sie behalten. (Oder er meldet "
"sie)",
author=user,
news_date=date.today()
))
if __name__ == "__main__":
with asyncio.Runner() as loop:
loop.run(run())
@@ -1,18 +1,18 @@
from functools import partial
from decimal import Decimal
from typing import Optional
from rio import Column, Component, event, Text, TextStyle, Button, Color, Revealer, Row, ProgressCircle, Link
from src.ez_lan_manager import ConfigurationService, UserService, AccountingService
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ez_lan_manager.types.SessionStorage import SessionStorage
from src.ez_lan_manager.types.Transaction import Transaction
from src.ez_lan_manager.types.User import User
from src.ezgg_lan_manager import ConfigurationService, UserService, AccountingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.types.Transaction import Transaction
from src.ezgg_lan_manager.types.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
class AccountPage(Component):
user: Optional[User] = None
balance: Optional[int] = None
balance: Optional[Decimal] = None
transaction_history: list[Transaction] = list()
banking_info_revealer_open: bool = False
paypal_info_revealer_open: bool = False
@@ -20,9 +20,14 @@ class AccountPage(Component):
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Guthabenkonto")
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
self.balance = await self.session[AccountingService].get_balance(self.user.user_id)
self.transaction_history = await self.session[AccountingService].get_transaction_history(self.user.user_id)
try:
user_id = self.session[UserSession].user_id
except KeyError:
pass
else:
self.user = await self.session[UserService].get_user(user_id)
self.balance = await self.session[AccountingService].get_balance(user_id)
self.transaction_history = await self.session[AccountingService].get_transaction_history(user_id)
async def _on_banking_info_press(self) -> None:
self.banking_info_revealer_open = not self.banking_info_revealer_open
@@ -159,7 +164,7 @@ class AccountPage(Component):
align_x=0
),
Text(
f"{'-' if transaction.is_debit else '+'}{AccountingService.make_euro_string_from_int(transaction.value)}",
f"{'-' if transaction.is_debit else '+'}{AccountingService.make_euro_string_from_decimal(transaction.value)}",
style=TextStyle(
fill=self.session.theme.danger_color if transaction.is_debit else self.session.theme.success_color,
font_size=0.8
@@ -175,7 +180,7 @@ class AccountPage(Component):
return Column(
MainViewContentBox(
content=Text(
f"Kontostand: {AccountingService.make_euro_string_from_int(self.balance)}",
f"Kontostand: {AccountingService.make_euro_string_from_decimal(self.balance)}",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
@@ -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
)
+105
View File
@@ -0,0 +1,105 @@
from __future__ import annotations
from typing import * # type: ignore
from rio import Component, event, Spacer, Card, Container, Column, Row, TextStyle, Color, Text, PageView, Button, Link
from src.ezgg_lan_manager import ConfigurationService, DatabaseService
from src.ezgg_lan_manager.components.DesktopNavigation import DesktopNavigation
class BasePage(Component):
color = "secondary"
corner_radius = (0, 0.5, 0, 0)
footer_size = 53.1
force_portrait_mode = False
@event.periodic(60)
async def check_db_conn(self) -> None:
is_healthy = await self.session[DatabaseService].is_healthy()
if not is_healthy:
self.session.navigate_to("./db-error")
@event.on_window_size_change
async def on_window_size_change(self):
self.force_refresh()
@event.on_page_change
def check_needed_size(self):
# ToDo: Low-Prio: Change layout, so the footer is always as wide as needed.
# This is a workaround, bc the seating page needs more width
if "/seating" in self.session.active_page_url.__str__():
self.footer_size = 78.2
else:
self.footer_size = 53.1
self.force_refresh()
def enforce_portrait_mode(self) -> None:
self.force_portrait_mode = True
self.force_refresh()
def build(self) -> Component:
content = Card(
PageView(),
color="secondary",
min_width=38,
corner_radius=(0, 0.5, 0, 0)
)
if self.session.window_width > 28 or self.force_portrait_mode:
return Container(
content=Column(
Column(
Row(
Spacer(grow_x=True, grow_y=True),
DesktopNavigation(),
content,
Spacer(grow_x=True, grow_y=True),
grow_y=True
),
Row(
Spacer(grow_x=True, grow_y=False),
Card(
content=Row(
Text(f"EZGG LAN Manager Version {self.session[ConfigurationService].APP_VERSION} © EZ GG e.V.", fill=self.session.theme.primary_color, style=TextStyle(font_size=0.5)),
Text(f"-", fill=self.session.theme.primary_color, style=TextStyle(font_size=0.5), margin_left=0.5, margin_right=0.5),
Link(content=Text(f"Impressum & DSGVO", fill=self.session.theme.primary_color, style=TextStyle(font_size=0.5)), target_url="./imprint"),
Text(f"-", fill=self.session.theme.primary_color, style=TextStyle(font_size=0.5), margin_left=0.5, margin_right=0.5),
Link(content=Text(f"Kontakt", fill=self.session.theme.primary_color, style=TextStyle(font_size=0.5)), target_url="./contact"),
align_x=0.5,
align_y=0.5
),
color=self.session.theme.neutral_color,
corner_radius=(0, 0, 0.5, 0.5),
grow_x=False,
grow_y=False,
min_height=1.2,
min_width=self.footer_size,
margin_bottom=3
),
Spacer(grow_x=True, grow_y=False),
grow_y=False
),
margin_top=4
)
),
grow_x=True,
grow_y=True
)
else:
return Column(
Text(
"Wir empfehlen auf\nmobilen Endgeräten im\nQuerformat zu arbeiten.\n\nBitte drehe dein Gerät.",
fill=Color.from_hex("FFFFFF"),
align_x=0.5,
align_y=0.5,
style=TextStyle(font_size=0.8)
),
Button(
content=Text("Ohne drehen fortfahren", margin=0.2),
style="minor",
shape="rounded",
align_x=0.5,
align_y=0,
on_press=self.enforce_portrait_mode
)
)
@@ -2,14 +2,14 @@ from typing import Optional
from rio import Text, Column, TextStyle, Component, event, Button, Popup
from src.ez_lan_manager import ConfigurationService, UserService, TicketingService
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ez_lan_manager.components.TicketBuyCard import TicketBuyCard
from src.ez_lan_manager.services.AccountingService import InsufficientFundsError
from src.ez_lan_manager.services.TicketingService import TicketNotAvailableError, UserAlreadyHasTicketError
from src.ez_lan_manager.types.SessionStorage import SessionStorage
from src.ez_lan_manager.types.Ticket import Ticket
from src.ez_lan_manager.types.User import User
from src.ezgg_lan_manager import ConfigurationService, UserService, TicketingService, RefreshService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.TicketBuyCard import TicketBuyCard
from src.ezgg_lan_manager.services.AccountingService import InsufficientFundsError
from src.ezgg_lan_manager.services.TicketingService import TicketNotAvailableError, UserAlreadyHasTicketError
from src.ezgg_lan_manager.types.Ticket import Ticket
from src.ezgg_lan_manager.types.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
class BuyTicketPage(Component):
@@ -19,14 +19,24 @@ class BuyTicketPage(Component):
popup_message: str = ""
is_popup_success: bool = False
is_buying_enabled: bool = False
is_user_logged_in: bool = False
@event.on_populate
async def on_populate(self) -> None:
self.session[RefreshService].subscribe(self.on_populate)
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Ticket kaufen")
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
try:
user_id = self.session[UserSession].user_id
except KeyError:
self.user = None
else:
self.user = await self.session[UserService].get_user(user_id)
if self.user is None: # No user logged in
self.is_buying_enabled = False
self.is_user_logged_in = False
self.user_ticket = None
else: # User is logged in
self.is_user_logged_in = True
possible_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id)
self.user_ticket = possible_ticket
if possible_ticket is not None: # User already has a ticket
@@ -67,17 +77,29 @@ class BuyTicketPage(Component):
def build(self) -> Component:
ticket_infos = self.session[ConfigurationService].get_ticket_info()
header = Text(
"Tickets & Preise",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
header = Column(
Text(
"Tickets & Preise",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
align_x=0.5
),
margin_top=2,
margin_bottom=2,
align_x=0.5
spacing=0.2
)
if not self.is_user_logged_in:
header.add(Text(
"Du musst eingeloggt sein\num ein Ticket zu kaufen",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.6
),
align_x=0.5
))
return Column(
MainViewContentBox(
Column(
@@ -1,13 +1,14 @@
from typing import Optional, Callable
from typing import Optional
from rio import Column, Component, event, TextStyle, Text, Spacer, Revealer, SwitcherBar, SwitcherBarChangeEvent, ProgressCircle
from src.ez_lan_manager import ConfigurationService, CateringService
from src.ez_lan_manager.components.CateringSelectionItem import CateringSelectionItem
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ez_lan_manager.components.ShoppingCartAndOrders import ShoppingCartAndOrders
from src.ez_lan_manager.types.CateringMenuItem import CateringMenuItemCategory, CateringMenuItem
from src.ez_lan_manager.types.SessionStorage import SessionStorage
from src.ezgg_lan_manager import ConfigurationService, CateringService
from src.ezgg_lan_manager.components.CateringSelectionItem import CateringSelectionItem
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.ShoppingCartAndOrders import ShoppingCartAndOrders
from src.ezgg_lan_manager.services.RefreshService import RefreshService
from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItemCategory, CateringMenuItem
from src.ezgg_lan_manager.types.UserSession import UserSession
class CateringPage(Component):
@@ -15,11 +16,9 @@ class CateringPage(Component):
all_menu_items: Optional[list[CateringMenuItem]] = None
shopping_cart_and_orders: list[ShoppingCartAndOrders] = []
def __post_init__(self) -> None:
self.session[SessionStorage].subscribe_to_logged_in_or_out_event(self.__class__.__name__, self.on_user_logged_in_status_changed)
@event.on_populate
async def on_populate(self) -> None:
self.session[RefreshService].subscribe(self.on_populate)
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Catering")
self.all_menu_items = await self.session[CateringService].get_menu()
@@ -34,7 +33,10 @@ class CateringPage(Component):
return list(filter(lambda item: item.category == category, all_menu_items))
def build(self) -> Component:
user_id = self.session[SessionStorage].user_id
try:
user_id = self.session[UserSession].user_id
except KeyError:
user_id = None
if len(self.shopping_cart_and_orders) == 0:
self.shopping_cart_and_orders.append(ShoppingCartAndOrders())
if len(self.shopping_cart_and_orders) > 1:
@@ -3,94 +3,93 @@ from typing import Optional
from rio import Text, Column, TextStyle, Component, event, TextInput, MultiLineTextInput, Row, Button
from src.ez_lan_manager import ConfigurationService, UserService, MailingService
from src.ez_lan_manager.components.AnimatedText import AnimatedText
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ez_lan_manager.types.SessionStorage import SessionStorage
from src.ez_lan_manager.types.User import User
from src.ezgg_lan_manager import ConfigurationService, UserService, MailingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.types.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
class ContactPage(Component):
# Workaround: Can not reassign this value without rio triggering refresh
# Using list to bypass this behavior
last_message_sent: list[datetime] = [datetime(day=1, month=1, year=2000)]
display_printing: list[bool] = [False]
user: Optional[User] = None
e_mail: str = ""
subject: str = ""
message: str = ""
submit_button_is_loading: bool = False
response_message: str = ""
is_success: bool = True
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Kontakt")
if self.session[SessionStorage].user_id is not None:
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
else:
try:
self.user = await self.session[UserService].get_user(self.session[UserSession].user_id)
except KeyError:
self.user = None
self.e_mail = "" if not self.user else self.user.user_mail
async def on_send_pressed(self) -> None:
error_msg = ""
self.submit_button.is_loading = True
self.submit_button.force_refresh()
self.submit_button_is_loading = True
now = datetime.now()
if not self.email_input.text:
if not self.e_mail:
error_msg = "E-Mail darf nicht leer sein!"
elif not self.subject_input.text:
elif not self.subject:
error_msg = "Betreff darf nicht leer sein!"
elif not self.message_input.text:
elif not self.message:
error_msg = "Nachricht darf nicht leer sein!"
elif (now - self.last_message_sent[0]) < timedelta(minutes=1):
error_msg = "Immer mit der Ruhe!"
if error_msg:
self.submit_button.is_loading = False
await self.animated_text.display_text(False, error_msg)
self.submit_button_is_loading = False
self.is_success = False
self.response_message = error_msg
return
mail_recipient = self.session[ConfigurationService].get_lan_info().organizer_mail
msg = (f"Kontaktformular vom {now.strftime('%d.%m.%Y %H:%M')}:\n\n"
f"Betreff: {self.subject_input.text}\n"
f"Absender: {self.email_input.text}\n\n"
f"Betreff: {self.subject}\n"
f"Absender: {self.e_mail}\n\n"
f"Inhalt:\n"
f"{self.message_input.text}\n")
f"{self.message}\n")
await self.session[MailingService].send_email("Kontaktformular-Mitteilung", msg, mail_recipient)
self.last_message_sent[0] = datetime.now()
self.submit_button.is_loading = False
await self.animated_text.display_text(True, "Nachricht erfolgreich gesendet!")
self.submit_button_is_loading = False
self.is_success = True
self.response_message = "Nachricht erfolgreich gesendet!"
def build(self) -> Component:
self.animated_text = AnimatedText(
margin_top=2,
margin_bottom=1,
align_x=0.1
)
self.email_input = TextInput(
email_input = TextInput(
label="E-Mail Adresse",
text="" if not self.user else self.user.user_mail,
text=self.bind().e_mail,
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True
)
self.subject_input = TextInput(
subject_input = TextInput(
label="Betreff",
text="",
text=self.bind().subject,
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True
)
self.message_input = MultiLineTextInput(
message_input = MultiLineTextInput(
label="Deine Nachricht an uns",
text="",
text=self.bind().message,
margin_left=1,
margin_right=1,
margin_bottom=1,
min_height=5
)
self.submit_button = Button(
submit_button = Button(
content=Text(
"Absenden",
style=TextStyle(fill=self.session.theme.success_color, font_size=0.9),
@@ -102,7 +101,8 @@ class ContactPage(Component):
shape="rectangle",
style="major",
color="primary",
on_press=self.on_send_pressed
on_press=self.on_send_pressed,
is_loading=self.bind().submit_button_is_loading
)
return Column(
MainViewContentBox(
@@ -117,12 +117,21 @@ class ContactPage(Component):
margin_bottom=1,
align_x=0.5
),
self.email_input,
self.subject_input,
self.message_input,
email_input,
subject_input,
message_input,
Row(
self.animated_text,
self.submit_button,
Text(
text=self.bind().response_message,
style=TextStyle(
fill=self.session.theme.success_color if self.is_success else self.session.theme.danger_color,
font_size=0.9
),
margin_top=2,
margin_bottom=1,
align_x=0.1
),
submit_button,
)
)
),
+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)
@@ -5,9 +5,9 @@ from typing import * # type: ignore
from rio import Component, event, Spacer, Card, Container, Column, Row, TextStyle, Color, Text
from src.ez_lan_manager.services.DatabaseService import DatabaseService
from src.ez_lan_manager import ConfigurationService
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
from src.ezgg_lan_manager import ConfigurationService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
class DbErrorPage(Component):
@@ -62,7 +62,7 @@ class DbErrorPage(Component):
Row(
Spacer(grow_x=True, grow_y=False),
Card(
content=Text(f"EZ LAN Manager Version {self.session[ConfigurationService].APP_VERSION} © EZ GG e.V.", align_x=0.5, align_y=0.5, style=TextStyle(fill=self.session.theme.primary_color, font_size=0.5)),
content=Text(f"EZGG LAN Manager Version {self.session[ConfigurationService].APP_VERSION} © EZ GG e.V.", align_x=0.5, align_y=0.5, style=TextStyle(fill=self.session.theme.primary_color, font_size=0.5)),
color=self.session.theme.neutral_color,
corner_radius=(0, 0, 0.5, 0.5),
grow_x=False,
@@ -82,7 +82,7 @@ class DbErrorPage(Component):
)
else:
return Text(
"Der EZ LAN Manager wird\nauf mobilen Endgeräten nur\nim Querformat unterstützt.\nBitte drehe dein Gerät.",
"Der EZGG LAN Manager wird\nauf mobilen Endgeräten nur\nim Querformat unterstützt.\nBitte drehe dein Gerät.",
align_x=0.5,
align_y=0.5,
style=TextStyle(fill=Color.from_hex("FFFFFF"), font_size=0.8)
+17
View File
@@ -0,0 +1,17 @@
from rio import Column, Component, event, Spacer
from src.ezgg_lan_manager import ConfigurationService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.UserEditForm import UserEditForm
class EditProfilePage(Component):
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Profil bearbeiten")
def build(self) -> Component:
return Column(
MainViewContentBox(UserEditForm(is_own_profile=True)),
Spacer(grow_y=True)
)
@@ -1,7 +1,7 @@
from rio import Column, Component, event, TextStyle, Text, Revealer
from src.ez_lan_manager import ConfigurationService
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager import ConfigurationService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
FAQ: list[list[str]] = [
["Wie melde ich mich für die LAN an?",
@@ -4,8 +4,8 @@ from random import choices
from email_validator import validate_email, EmailNotValidError
from rio import Column, Component, event, Text, TextStyle, TextInput, TextInputChangeEvent, Button
from src.ez_lan_manager import ConfigurationService, UserService, MailingService
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager import ConfigurationService, UserService, MailingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
class ForgotPasswordPage(Component):
@@ -27,11 +27,11 @@ class ForgotPasswordPage(Component):
user = await user_service.get_user(self.email_input.text.strip())
if user is not None:
new_password = "".join(choices(user_service.ALLOWED_USER_NAME_SYMBOLS, k=16))
user.user_password = sha256(new_password.encode(encoding="utf-8")).hexdigest()
user.user_fallback_password = sha256(new_password.encode(encoding="utf-8")).hexdigest()
await user_service.update_user(user)
await mailing_service.send_email(
subject=f"Dein neues Passwort für {lan_info.name}",
body=f"Du hast für den EZ-LAN Manager der {lan_info.name} ein neues Passwort angefragt. "
body=f"Du hast für den EZGG-LAN Manager der {lan_info.name} ein neues Passwort angefragt. "
f"Und hier ist es schon:\n\n{new_password}\n\nSolltest du kein neues Passwort angefordert haben, "
f"ignoriere diese E-Mail.\n\nLiebe Grüße\nDein {lan_info.name} - Team",
receiver=self.email_input.text.strip()
@@ -2,10 +2,10 @@ from typing import Optional
from rio import Column, Component, event, TextStyle, Text, Button, Row, TextInput, Spacer, TextInputChangeEvent
from src.ez_lan_manager import ConfigurationService, UserService, TicketingService, SeatingService
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ez_lan_manager.types.Seat import Seat
from src.ez_lan_manager.types.User import User
from src.ezgg_lan_manager import ConfigurationService, UserService, TicketingService, SeatingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.types.Seat import Seat
from src.ezgg_lan_manager.types.User import User
class GuestsPage(Component):
@@ -1,7 +1,7 @@
from rio import Text, Column, TextStyle, Component, event, Link, Color
from src.ez_lan_manager import ConfigurationService
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager import ConfigurationService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
class ImprintPage(Component):
@@ -1,26 +1,76 @@
import logging
from dataclasses import field, dataclass
from datetime import datetime
from typing import Optional
from typing import Optional, Callable
from rio import Column, Component, event, TextStyle, Text, Spacer, PointerEvent, Button
from rio import Column, Component, event, TextStyle, Text, Spacer, PointerEvent, Button, Popup, Card, Row
from src.ez_lan_manager import ConfigurationService, CateringService, SeatingService
from src.ez_lan_manager.components.CateringManagementOrderDisplay import CateringManagementOrderDisplay
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ez_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus
from src.ez_lan_manager.types.Seat import Seat
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.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus
from src.ezgg_lan_manager.types.Seat import Seat
logger = logging.getLogger(__name__.split(".")[-1])
class CateringOrderInfoPopup(Component):
order: Optional[CateringOrder] = None
close_cb: Optional[Callable] = None
def build(self) -> Component:
if not self.order:
return Card(
content=Text(""),
margin=1,
color=self.session.theme.hud_color,
min_width=40,
min_height=40,
on_press=self.close_cb
)
rows = []
is_contrast_line = True
for item, amount in self.order.items.items():
style = TextStyle(
fill=self.session.theme.secondary_color if is_contrast_line else self.session.theme.neutral_color)
is_contrast_line = not is_contrast_line
rows.append(
Row(
Text(f"{amount}x", style=style),
Spacer(),
Text(f"{item.name}", style=style)
)
)
return Card(
content=Column(
Text(f"Bestellung {self.order.order_id}", style=TextStyle(font_size=1.2), margin_bottom=1),
*rows,
Spacer(),
Row(Text("Gesamtpreis:"), Spacer(),
Text(self.session[AccountingService].make_euro_string_from_decimal(self.order.price)))
),
margin=1,
color=self.session.theme.hud_color,
min_width=40,
min_height=40,
on_press=self.close_cb,
corner_radius=0.5,
elevate_on_hover=False,
colorize_on_hover=False
)
@dataclass
class CateringOrderWithSeat:
catering_order: CateringOrder
seat: Optional[Seat]
class ManageCateringPage(Component):
all_orders: list[CateringOrderWithSeat] = field(default_factory=list)
last_updated: Optional[datetime] = None
order_popup_open: bool = False
order_popup_order: Optional[CateringOrder] = None
@event.on_populate
async def on_populate(self) -> None:
@@ -28,7 +78,6 @@ class ManageCateringPage(Component):
self.all_orders = await self.populate_seating(await self.session[CateringService].get_orders())
self.last_updated = datetime.now()
@event.periodic(30)
async def update_orders(self) -> None:
polled_orders = await self.session[CateringService].get_orders()
@@ -43,34 +92,47 @@ class ManageCateringPage(Component):
return result
def get_all_pending_orders(self) -> list[CateringOrderWithSeat]:
filtered_list = list(filter(lambda o: o.catering_order.status != CateringOrderStatus.COMPLETED and o.catering_order.status != CateringOrderStatus.CANCELED, self.all_orders))
filtered_list = list(filter(lambda
o: o.catering_order.status != CateringOrderStatus.COMPLETED and o.catering_order.status != CateringOrderStatus.CANCELED,
self.all_orders))
sorted_list = sorted(filtered_list, key=lambda o: o.catering_order.order_date)
return sorted_list
def get_all_completed_orders(self) -> list[CateringOrderWithSeat]:
filtered_list = list(filter(lambda o: o.catering_order.status == CateringOrderStatus.COMPLETED or o.catering_order.status == CateringOrderStatus.CANCELED, self.all_orders))
filtered_list = list(filter(lambda
o: o.catering_order.status == CateringOrderStatus.COMPLETED or o.catering_order.status == CateringOrderStatus.CANCELED,
self.all_orders))
sorted_list = sorted(filtered_list, key=lambda o: o.catering_order.order_date)
return sorted_list
async def order_clicked(self, order: CateringOrder, _: PointerEvent) -> None:
pass
self.order_popup_order = order
self.order_popup_open = True
async def close_cb(self) -> None:
self.order_popup_open = False
def build(self) -> Component:
header_text = Text(
text="Catering Verwaltung",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
)
popup = Popup(
anchor=header_text,
content=CateringOrderInfoPopup(order=self.order_popup_order, close_cb=self.close_cb),
is_open=self.order_popup_open,
position="bottom",
corner_radius=0.5
)
return Column(
MainViewContentBox(
Column(
Text(
text="Catering Verwaltung",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
)
)
Column(popup)
),
MainViewContentBox(
Column(
@@ -100,7 +162,8 @@ class ManageCateringPage(Component):
margin_bottom=1,
on_press=self.update_orders
),
*[CateringManagementOrderDisplay(v.catering_order, v.seat, self.order_clicked) for v in self.get_all_pending_orders()],
*[CateringManagementOrderDisplay(v.catering_order, v.seat, self.order_clicked) for v in
self.get_all_pending_orders()],
)
),
MainViewContentBox(
@@ -115,7 +178,8 @@ class ManageCateringPage(Component):
margin_bottom=0.2,
align_x=0.5
),
*[CateringManagementOrderDisplay(v.catering_order, v.seat, self.order_clicked) for v in self.get_all_completed_orders()],
*[CateringManagementOrderDisplay(v.catering_order, v.seat, self.order_clicked) for v in
self.get_all_completed_orders()],
)
),
Spacer()
@@ -5,11 +5,11 @@ from time import strptime
from rio import Column, Component, event, TextStyle, Text
from src.ez_lan_manager import ConfigurationService, UserService
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ez_lan_manager.components.NewsPost import EditableNewsPost
from src.ez_lan_manager.services.NewsService import NewsService
from src.ez_lan_manager.types.News import News
from src.ezgg_lan_manager import ConfigurationService, UserService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.NewsPost import EditableNewsPost
from src.ezgg_lan_manager.services.NewsService import NewsService
from src.ezgg_lan_manager.types.News import News
logger = logging.getLogger(__name__.split(".")[-1])
@@ -78,8 +78,8 @@ class ManageNewsPage(Component):
Column(
Text(
text="News Verwaltung",
fill=self.session.theme.background_color,
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
@@ -88,8 +88,8 @@ class ManageNewsPage(Component):
),
Text(
text="Neuen News Post erstellen",
fill=self.session.theme.background_color,
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.1
),
margin_top=2,
@@ -106,8 +106,8 @@ class ManageNewsPage(Component):
),
Text(
text="Post erfolgreich erstellt",
fill=self.session.theme.success_color,
style=TextStyle(
fill=self.session.theme.success_color,
font_size=0.7 if self.show_success_message else 0
),
margin_top=0.1,
@@ -116,8 +116,8 @@ class ManageNewsPage(Component):
),
Text(
text="Bisherige Posts",
fill=self.session.theme.background_color,
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.1
),
margin_top=2,
@@ -0,0 +1,121 @@
import logging
from datetime import datetime
from functools import partial
from typing import Optional
from from_root import from_root
from rio import Column, Component, event, TextStyle, Text, Spacer, Row, Image, Tooltip, IconButton, Popup, Rectangle, Dropdown, ThemeContextSwitcher, Button
from src.ezgg_lan_manager import ConfigurationService, TournamentService, UserService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.types.DateUtil import weekday_to_display_text
from src.ezgg_lan_manager.types.Participant import Participant
from src.ezgg_lan_manager.types.Tournament import Tournament
from src.ezgg_lan_manager.types.TournamentBase import TournamentStatus
logger = logging.getLogger(__name__.split(".")[-1])
class ManageTournamentsPage(Component):
tournaments: list[Tournament] = []
remove_participant_popup_open: bool = False
cancel_options: dict[str, Optional[Participant]] = {"": None}
tournament_id_selected_for_participant_removal: Optional[int] = None
participant_selected_for_removal: Optional[Participant] = None
@event.on_populate
async def on_populate(self) -> None:
self.tournaments = await self.session[TournamentService].get_tournaments()
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Turnier Verwaltung")
async def on_start_pressed(self, tournament_id: int) -> None:
logger.info(f"Starting tournament with ID {tournament_id}")
await self.session[TournamentService].start_tournament(tournament_id)
async def on_cancel_pressed(self, tournament_id: int) -> None:
logger.info(f"Canceling tournament with ID {tournament_id}")
await self.session[TournamentService].cancel_tournament(tournament_id)
async def on_remove_participant_pressed(self, tournament_id: int) -> None:
tournament = await self.session[TournamentService].get_tournament_by_id(tournament_id)
if tournament is None:
return
users = await self.session[UserService].get_all_users()
try:
cancel_options = {next(filter(lambda u: u.user_id == p.id, users)).user_name: p for p in tournament.participants}
if cancel_options:
self.cancel_options = cancel_options
else:
self.cancel_options = {"": None}
except StopIteration as e:
logger.error(f"Error trying to find user for participant: {e}")
self.tournament_id_selected_for_participant_removal = tournament_id
self.remove_participant_popup_open = True
async def on_remove_participant_confirm_pressed(self) -> None:
if self.participant_selected_for_removal is not None and self.tournament_id_selected_for_participant_removal is not None:
logger.info(f"Removing participant with ID {self.participant_selected_for_removal.id} from tournament with ID {self.tournament_id_selected_for_participant_removal}")
await self.session[TournamentService].unregister_user_from_tournament(self.participant_selected_for_removal.id, self.tournament_id_selected_for_participant_removal)
await self.on_remove_participant_cancel_pressed()
async def on_remove_participant_cancel_pressed(self) -> None:
self.tournament_id_selected_for_participant_removal = None
self.participant_selected_for_removal = None
self.remove_participant_popup_open = False
def build(self) -> Component:
tournament_rows = []
for tournament in self.tournaments:
start_time_color = self.session.theme.background_color
if tournament.start_time < datetime.now() and tournament.status == TournamentStatus.OPEN:
start_time_color = self.session.theme.warning_color
tournament_rows.append(
Row(
Image(image=from_root(f"src/ezgg_lan_manager/assets/img/games/{tournament.game_title.image_name}"), min_width=1.5, margin_right=1),
Text(tournament.name, style=TextStyle(fill=self.session.theme.background_color, font_size=0.8), justify="left", margin_right=1.5),
Text(f"{weekday_to_display_text(tournament.start_time.weekday())[:2]}.{tournament.start_time.strftime('%H:%M')} Uhr", style=TextStyle(fill=start_time_color, font_size=0.8), justify="left", margin_right=1),
Spacer(),
Tooltip(anchor=IconButton("material/play_arrow", min_size=2, margin_right=0.5, on_press=partial(self.on_start_pressed, tournament.id)), tip="Starten"),
Tooltip(anchor=IconButton("material/cancel_schedule_send", min_size=2, margin_right=0.5, on_press=partial(self.on_cancel_pressed, tournament.id)), tip="Absagen"),
Tooltip(anchor=IconButton("material/person_cancel", min_size=2, on_press=partial(self.on_remove_participant_pressed, tournament.id)), tip="Spieler entfernen"),
margin=1
)
)
return Column(
Popup(
anchor=MainViewContentBox(
Column(
Text(
text="Turnier Verwaltung",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
),
*tournament_rows
)
),
content=Rectangle(
content=Row(
ThemeContextSwitcher(
content=Dropdown(options=self.cancel_options, min_width=20, selected_value=self.bind().participant_selected_for_removal), color=self.session.theme.hud_color
),
Button(content="REMOVE", shape="rectangle", grow_x=False, on_press=self.on_remove_participant_confirm_pressed),
Button(content="CANCEL", shape="rectangle", grow_x=False, on_press=self.on_remove_participant_cancel_pressed),
margin=0.5
),
min_width=30,
min_height=4,
fill=self.session.theme.primary_color,
margin_top=3.5,
stroke_width=0.3,
stroke_color=self.session.theme.neutral_color,
),
is_open=self.remove_participant_popup_open,
color="none"
),
Spacer()
)
@@ -3,20 +3,21 @@ from dataclasses import field
from typing import Optional
from rio import Column, Component, event, TextStyle, Text, TextInput, ThemeContextSwitcher, Grid, \
PointerEventListener, PointerEvent, Rectangle, CursorStyle, Color, TextInputChangeEvent, Spacer, Row, Switch, \
PointerEventListener, PointerEvent, Rectangle, Color, TextInputChangeEvent, Spacer, Row, Switch, \
SwitchChangeEvent, EventHandler
from src.ez_lan_manager import ConfigurationService, UserService, AccountingService, SeatingService
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ez_lan_manager.components.NewTransactionForm import NewTransactionForm
from src.ez_lan_manager.components.UserEditForm import UserEditForm
from src.ez_lan_manager.services.AccountingService import InsufficientFundsError
from src.ez_lan_manager.types.SessionStorage import SessionStorage
from src.ez_lan_manager.types.Transaction import Transaction
from src.ez_lan_manager.types.User import User
from src.ezgg_lan_manager import ConfigurationService, UserService, AccountingService, SeatingService, MailingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.NewTransactionForm import NewTransactionForm
from src.ezgg_lan_manager.components.UserEditForm import UserEditForm
from src.ezgg_lan_manager.services.AccountingService import InsufficientFundsError
from src.ezgg_lan_manager.types.Transaction import Transaction
from src.ezgg_lan_manager.types.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
logger = logging.getLogger(__name__.split(".")[-1])
class ClickableGridContent(Component):
text: str = ""
is_hovered: bool = False
@@ -36,17 +37,19 @@ class ClickableGridContent(Component):
content=Rectangle(
content=Text(
self.text,
style=TextStyle(fill=self.session.theme.success_color) if self.is_hovered else TextStyle(fill=self.session.theme.background_color),
style=TextStyle(fill=self.session.theme.success_color) if self.is_hovered else TextStyle(
fill=self.session.theme.background_color),
grow_x=True
),
fill=Color.TRANSPARENT,
cursor=CursorStyle.POINTER
cursor="pointer"
),
on_pointer_enter=self.on_mouse_enter,
on_pointer_leave=self.on_mouse_leave,
on_press=self.on_mouse_click
)
class ManageUsersPage(Component):
selected_user: Optional[User] = None
all_users: Optional[list] = None
@@ -66,25 +69,31 @@ class ManageUsersPage(Component):
async def on_user_clicked(self, user_name: str) -> None:
self.selected_user = next(filter(lambda user: user.user_name == user_name, self.all_users))
user_account_balance_raw = await self.session[AccountingService].get_balance(self.selected_user.user_id)
self.user_account_balance = AccountingService.make_euro_string_from_int(user_account_balance_raw)
self.user_account_balance = AccountingService.make_euro_string_from_decimal(user_account_balance_raw)
seat = await self.session[SeatingService].get_user_seat(self.selected_user.user_id)
self.user_seat = seat.seat_id if seat else "-"
self.is_user_account_locked = not self.selected_user.is_active
async def on_search_parameters_changed(self, e: TextInputChangeEvent) -> None:
self.search_results = list(filter(lambda user: (e.text.lower() in user.user_name.lower()) or e.text.lower() in str(user.user_id), self.all_users))
self.search_results = list(
filter(lambda user: (e.text.lower() in user.user_name.lower()) or e.text.lower() in str(user.user_id),
self.all_users))
async def change_account_active(self, _: SwitchChangeEvent) -> None:
self.selected_user.is_active = not self.is_user_account_locked
await self.session[UserService].update_user(self.selected_user)
async def on_new_transaction(self, transaction: Transaction) -> None:
if not self.session[SessionStorage].is_team_member: # Better safe than sorry
try:
user = await self.session[UserService].get_user(self.session[UserSession].user_id)
if not user.is_team_member: # Better safe than sorry
return
except KeyError:
return
logger.info(f"Got new transaction for user with ID '{transaction.user_id}' over "
f"{'-' if transaction.is_debit else '+'}"
f"{AccountingService.make_euro_string_from_int(transaction.value)} "
f"{AccountingService.make_euro_string_from_decimal(transaction.value)} "
f"with reference '{transaction.reference}'")
if transaction.is_debit:
@@ -99,16 +108,21 @@ class ManageUsersPage(Component):
self.accounting_section_result_success = False
return
else:
await self.session[AccountingService].add_balance(
new_total_balance = await self.session[AccountingService].add_balance(
transaction.user_id,
transaction.value,
transaction.reference
)
user = await self.session[UserService].get_user(transaction.user_id)
await self.session[MailingService].send_email(
"Dein Guthaben wurde aufgeladen",
self.session[MailingService].generate_account_balance_added_mail_body(user, transaction.value, new_total_balance),
user.user_mail
)
self.accounting_section_result_text = f"Guthaben {'entfernt' if transaction.is_debit else 'hinzugefügt'}!"
self.accounting_section_result_success = True
def build(self) -> Component:
return Column(
MainViewContentBox(
@@ -1,8 +1,8 @@
from rio import Column, Component, event
from src.ez_lan_manager import ConfigurationService, NewsService
from src.ez_lan_manager.components.NewsPost import NewsPost
from src.ez_lan_manager.types.News import News
from src.ezgg_lan_manager import ConfigurationService, NewsService
from src.ezgg_lan_manager.components.NewsPost import NewsPost
from src.ezgg_lan_manager.types.News import News
class NewsPage(Component):
+129
View File
@@ -0,0 +1,129 @@
from rio import Column, Component, event, Text, Spacer, Row, Link
from src.ezgg_lan_manager import ConfigurationService, TicketingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
class OverviewPage(Component):
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Übersicht")
def build(self) -> Component:
lan_info = self.session[ConfigurationService].get_lan_info()
return Column(
MainViewContentBox(
Column(
Text(lan_info.name, font_size=2, justify="center", fill=self.session.theme.neutral_color, margin_top=0.5),
Text(f"Edition {lan_info.iteration}", font_size=0.9, justify="center", fill=self.session.theme.neutral_color, margin_bottom=1.5)
)
),
MainViewContentBox(
Column(
Text("Allgemeines", font_size=2, justify="center", fill=self.session.theme.neutral_color, margin_top=0.5, margin_bottom=1),
Column(
Row(
Text("Wann?", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Text(f"{lan_info.date_from.strftime("%d.%m.")} bis {lan_info.date_till.strftime("%d.%m.%Y")}", fill=self.session.theme.neutral_color, margin_right=1),
margin_bottom=0.3
),
Row(
Text("Wo?", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Link(Text(f"DGH Donsbach", fill=self.session.theme.secondary_color, margin_right=1), target_url="https://maps.app.goo.gl/3Zyue776A22jdoxz5", open_in_new_tab=True),
margin_bottom=0.3
),
Row(
Text("Einlass", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Text(lan_info.date_from.strftime("Freitag %H:%M Uhr"), fill=self.session.theme.neutral_color, margin_right=1),
margin_bottom=0.3
),
Row(
Text("Ende", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Text(lan_info.date_till.strftime("Sonntag %H:%M Uhr"), fill=self.session.theme.neutral_color, margin_right=1),
margin_bottom=0.3
),
Row(
Text("Anmeldung", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Text("Geöffnet", fill=self.session.theme.success_color, margin_right=1),
margin_bottom=0.3
),
Row(
Text("Teilnehmer", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Text(str(self.session[TicketingService].get_total_tickets()), fill=self.session.theme.neutral_color, margin_right=1),
margin_bottom=0.3
)
,
Row(
Text("Ticket Preise", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Link(Text(f"Preisliste", fill=self.session.theme.secondary_color, margin_right=1), target_url="./buy_ticket", open_in_new_tab=False),
margin_bottom=0.3
)
)
)
),
MainViewContentBox(
Column(
Text("Technisches", font_size=2, justify="center", fill=self.session.theme.neutral_color, margin_top=0.5, margin_bottom=1),
Column(
Row(
Text("Internet", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Text(f"100/50 Mbit/s (down/up)", fill=self.session.theme.neutral_color, margin_right=1),
margin_bottom=0.3
),
Row(
Text("Routing", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Text(f"Flaches Netz", fill=self.session.theme.neutral_color, margin_right=1),
margin_bottom=0.3
),
Row(
Text("WLAN", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Text(f"vorhanden", fill=self.session.theme.neutral_color, margin_right=1),
margin_bottom=0.3
)
)
)
),
MainViewContentBox(
Column(
Text("Sonstiges", font_size=2, justify="center", fill=self.session.theme.neutral_color, margin_top=0.5, margin_bottom=1),
Column(
Row(
Text("Schlafen", fill=self.session.theme.neutral_color, margin_left=1, justify="center"),
margin_bottom=0.3
),
Row(
Text("Es steht ein Schlafsaal zur Verfügung. Nach der Eröffnung steht auch die Bühne als Schlafbereich zur Verfügung.", font_size=0.7,
fill=self.session.theme.neutral_color, margin_left=1, overflow="wrap"),
margin_bottom=0.3
),
Row(
Text("Essen & Trinken", fill=self.session.theme.neutral_color, margin_left=1, justify="center"),
margin_bottom=0.3
),
Row(
Text("Wir sorgen für euer leibliches Wohl, ihr dürft aber auch eure eigenen Speißen und Getränke mitbringen.", font_size=0.7, fill=self.session.theme.neutral_color, margin_left=1, overflow="wrap"),
margin_bottom=0.3
),
Row(
Text("Parken", fill=self.session.theme.neutral_color, margin_left=1, justify="center"),
margin_bottom=0.3
),
Row(
Text("Vor der Halle sind ausreichend Parkplätze vorhanden.", font_size=0.7, fill=self.session.theme.neutral_color, margin_left=1, overflow="wrap"),
margin_bottom=0.3
)
)
)
),
Spacer()
)
@@ -1,7 +1,7 @@
from rio import Column, Component, event
from src.ez_lan_manager import ConfigurationService
from src.ez_lan_manager.components.NewsPost import NewsPost
from src.ezgg_lan_manager import ConfigurationService
from src.ezgg_lan_manager.components.NewsPost import NewsPost
class PlaceholderPage(Component):
+202
View File
@@ -0,0 +1,202 @@
import logging
from asyncio import sleep, create_task
from email_validator import validate_email, EmailNotValidError
from rio import Column, Component, event, Text, TextStyle, TextInput, TextInputChangeEvent, Button
from src.ezgg_lan_manager import ConfigurationService, UserService, MailingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
MINIMUM_PASSWORD_LENGTH = 6
logger = logging.getLogger(__name__.split(".")[-1])
class RegisterPage(Component):
pw_1: str = ""
pw_2: str = ""
email: str = ""
user_name: str = ""
pw_1_valid: bool = True
pw_2_valid: bool = True
email_valid: bool = True
submit_button_loading: bool = False
display_text: str = ""
display_text_style: TextStyle = TextStyle()
def on_pw_focus_loss(self, _: TextInputChangeEvent) -> None:
if not (self.pw_1 == self.pw_2) or len(self.pw_1) < MINIMUM_PASSWORD_LENGTH:
self.pw_1_valid = False
self.pw_2_valid = False
return
self.pw_1_valid = True
self.pw_2_valid = True
def on_email_focus_loss(self, change_event: TextInputChangeEvent) -> None:
try:
validate_email(change_event.text, check_deliverability=False)
self.email_valid = True
except EmailNotValidError:
self.email_valid = False
def on_user_name_focus_loss(self, _: TextInputChangeEvent) -> None:
current_text = self.user_name
if len(current_text) > UserService.MAX_USERNAME_LENGTH:
self.user_name = current_text[:UserService.MAX_USERNAME_LENGTH]
async def on_submit_button_pressed(self) -> None:
self.submit_button_loading = True
if len(self.user_name) < 1:
await self.display_animated_text(False, "Nutzername darf nicht leer sein!")
self.submit_button_loading = False
return
if not (self.pw_1 == self.pw_2):
await self.display_animated_text(False, "Passwörter stimmen nicht überein!")
self.submit_button_loading = False
return
if len(self.pw_1) < MINIMUM_PASSWORD_LENGTH:
await self.display_animated_text(False, f"Passwort muss mindestens {MINIMUM_PASSWORD_LENGTH} Zeichen lang sein!")
self.submit_button_loading = False
return
if not self.email_valid or len(self.email) < 3:
await self.display_animated_text(False, "E-Mail Adresse ungültig!")
self.submit_button_loading = False
return
user_service = self.session[UserService]
mailing_service = self.session[MailingService]
lan_info = self.session[ConfigurationService].get_lan_info()
if await user_service.get_user(self.email) is not None or await user_service.get_user(self.user_name) is not None:
await self.display_animated_text(False, "Benutzername oder E-Mail bereits registriert!")
self.submit_button_loading = False
return
try:
new_user = await user_service.create_user(self.user_name, self.email, self.pw_1)
if not new_user:
logger.error(f"create_user returned: {new_user}")
raise Exception(f"create_user returned: {new_user}")
except Exception as e:
logger.error(f"Unknown error during new user registration: {e}")
await self.display_animated_text(False, "Es ist ein unbekannter Fehler aufgetreten :(")
self.submit_button_loading = False
return
await mailing_service.send_email(
subject="Erfolgreiche Registrierung",
body=f"Hallo {self.user_name},\n\n"
f"Du hast dich erfolgreich beim EZGG-LAN Manager für {lan_info.name} {lan_info.iteration} registriert.\n\n"
f"Wenn du dich nicht registriert hast, kontaktiere bitte unser Team über unsere Homepage.\n\n"
f"Liebe Grüße\nDein {lan_info.name} - Team",
receiver=self.email
)
self.submit_button_loading = False
await self.display_animated_text(True, "Erfolgreich registriert!")
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Registrieren")
async def display_animated_text(self, success: bool, text: str) -> None:
self.display_text = ""
style = TextStyle(
fill=self.session.theme.success_color if success else self.session.theme.danger_color,
font_size=0.9
)
self.display_text_style = style
_ = create_task(self._animate_text(text))
async def _animate_text(self, text: str) -> None:
for c in text:
self.display_text += c
await sleep(0.06)
def build(self) -> Component:
user_name_input = TextInput(
label="Benutzername",
text=self.bind().user_name,
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
on_lose_focus=self.on_user_name_focus_loss
)
email_input = TextInput(
label="E-Mail Adresse",
text=self.bind().email,
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
on_lose_focus=self.on_email_focus_loss,
is_valid=self.email_valid
)
pw_1_input = TextInput(
label="Passwort",
text=self.bind().pw_1,
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
is_secret=True,
on_lose_focus=self.on_pw_focus_loss,
is_valid=self.pw_1_valid
)
pw_2_input = TextInput(
label="Passwort wiederholen",
text=self.bind().pw_2,
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
is_secret=True,
on_lose_focus=self.on_pw_focus_loss,
is_valid=self.pw_2_valid
)
submit_button = Button(
content=Text(
"Registrieren",
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9),
align_x=0.5
),
grow_x=True,
margin_top=2,
margin_left=1,
margin_right=1,
margin_bottom=1,
shape="rectangle",
style="minor",
color=self.session.theme.secondary_color,
on_press=self.on_submit_button_pressed,
is_loading=self.submit_button_loading
)
return Column(
MainViewContentBox(
content=Column(
Text(
"Neues Konto anlegen",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
),
user_name_input,
email_input,
pw_1_input,
pw_2_input,
submit_button,
Text(self.display_text, margin_top=2, margin_left=1, margin_right=1, margin_bottom=2, style=self.display_text_style)
)
),
align_y=0,
)
@@ -1,7 +1,7 @@
from rio import Column, Component, event, TextStyle, Text, Revealer
from src.ez_lan_manager import ConfigurationService
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager import ConfigurationService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
RULES: list[str] = [
"Respektvolles Verhalten: Sei höflich und respektvoll gegenüber anderen Gästen und dem Team.",
@@ -1,18 +1,19 @@
import logging
from asyncio import sleep
from decimal import Decimal
from typing import Optional
from rio import Text, Column, TextStyle, Component, event, PressEvent, ProgressCircle
from src.ez_lan_manager import ConfigurationService, SeatingService, TicketingService, UserService
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ez_lan_manager.components.SeatingPlan import SeatingPlan, SeatingPlanLegend
from src.ez_lan_manager.components.SeatingPlanInfoBox import SeatingPlanInfoBox
from src.ez_lan_manager.components.SeatingPurchaseBox import SeatingPurchaseBox
from src.ez_lan_manager.services.SeatingService import NoTicketError, SeatNotFoundError, WrongCategoryError, SeatAlreadyTakenError
from src.ez_lan_manager.types.Seat import Seat
from src.ez_lan_manager.types.SessionStorage import SessionStorage
from src.ez_lan_manager.types.User import User
from src.ezgg_lan_manager import ConfigurationService, SeatingService, TicketingService, UserService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.SeatingPlan import SeatingPlan, SeatingPlanLegend
from src.ezgg_lan_manager.components.SeatingPlanInfoBox import SeatingPlanInfoBox
from src.ezgg_lan_manager.components.SeatingPurchaseBox import SeatingPurchaseBox
from src.ezgg_lan_manager.services.SeatingService import NoTicketError, SeatNotFoundError, WrongCategoryError, SeatAlreadyTakenError
from src.ezgg_lan_manager.types.Seat import Seat
from src.ezgg_lan_manager.types.User import User
from src.ezgg_lan_manager.types.UserSession import UserSession
logger = logging.getLogger(__name__.split(".")[-1])
@@ -21,7 +22,7 @@ class SeatingPlanPage(Component):
seating_info: Optional[list[Seat]] = None
current_seat_id: Optional[str] = None
current_seat_occupant: Optional[str] = None
current_seat_price: int = 0
current_seat_price: Decimal = Decimal("0")
current_seat_is_blocked: bool = False
user: Optional[User] = None
show_info_box: bool = True
@@ -29,13 +30,17 @@ class SeatingPlanPage(Component):
purchase_box_loading: bool = False
purchase_box_success_msg: Optional[str] = None
purchase_box_error_msg: Optional[str] = None
seating_info_text = ""
is_booking_blocked: bool = False
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Sitzplan")
self.seating_info = await self.session[SeatingService].get_seating()
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
try:
self.user = await self.session[UserService].get_user(self.session[UserSession].user_id)
except KeyError:
self.user = None
if not self.user:
self.is_booking_blocked = True
else:
@@ -46,6 +51,7 @@ class SeatingPlanPage(Component):
self.is_booking_blocked = True
async def on_seat_clicked(self, seat_id: str, _: PressEvent) -> None:
self.seating_info_text = ""
self.show_info_box = True
self.show_purchase_box = False
seat = next(filter(lambda s: s.seat_id == seat_id, self.seating_info), None)
@@ -54,13 +60,19 @@ class SeatingPlanPage(Component):
self.current_seat_is_blocked = seat.is_blocked
self.current_seat_id = seat.seat_id
ticket_info = self.session[TicketingService].get_ticket_info_by_category(seat.category)
price = 0 if not ticket_info else ticket_info.price
price = Decimal("0") if not ticket_info else ticket_info.price
self.current_seat_price = price
if seat.user:
self.current_seat_occupant = seat.user.user_name
else:
self.current_seat_occupant = None
async def on_info_clicked(self, text: str) -> None:
self.show_info_box = True
self.show_purchase_box = False
self.current_seat_id = None
self.seating_info_text = text
def set_error(self, msg: str) -> None:
self.purchase_box_error_msg = msg
self.purchase_box_success_msg = None
@@ -118,7 +130,7 @@ class SeatingPlanPage(Component):
Column(
SeatingPlanInfoBox(seat_id=self.current_seat_id, seat_occupant=self.current_seat_occupant, seat_price=self.current_seat_price,
is_blocked=self.current_seat_is_blocked, is_booking_blocked=self.is_booking_blocked, show=self.show_info_box,
purchase_cb=self.on_purchase_clicked),
purchase_cb=self.on_purchase_clicked, override_text=self.seating_info_text),
SeatingPurchaseBox(
show=self.show_purchase_box,
seat_id=self.current_seat_id,
@@ -131,7 +143,7 @@ class SeatingPlanPage(Component):
)
),
MainViewContentBox(
SeatingPlan(seat_clicked_cb=self.on_seat_clicked, seating_info=self.seating_info) if self.seating_info else
SeatingPlan(seat_clicked_cb=self.on_seat_clicked, seating_info=self.seating_info, info_clicked_cb=self.on_info_clicked) if self.seating_info else
Column(ProgressCircle(color=self.session.theme.secondary_color, margin=3),
Text("Sitzplan wird geladen", style=TextStyle(fill=self.session.theme.neutral_color), align_x=0.5, margin=1))
),
@@ -1,7 +1,7 @@
from rio import Column, Component, event, TextStyle, Text
from src.ez_lan_manager import ConfigurationService
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager import ConfigurationService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
class PAGENAME(Component):

Some files were not shown because too many files have changed in this diff Show More