Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| afe5575b34 | |||
| 9da25d4197 | |||
| cf9b39ba0a | |||
| 3655e8eb53 | |||
| 43ce42052e | |||
| 0ca06c244c |
@@ -1,22 +0,0 @@
|
|||||||
<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ühren zurzeit Wartungsarbeiten durch und sind in kürze wieder für euch da.</p>
|
|
||||||
<p>— Euer EZGG LAN Team</p>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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", "--"]
|
|
||||||
@@ -1,49 +1,13 @@
|
|||||||
# EZGG LAN Manager
|
# EZ LAN Manager
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This repository contains the code for the EZGG LAN Manager.
|
This repository contains the code for the EZ LAN Manager.
|
||||||
|
|
||||||
## Development Setup
|
## How to install [prod]
|
||||||
|
|
||||||
### Prerequisites
|
TBD
|
||||||
|
|
||||||
- Working Installation of MariaDB Server (version `10.6.25` or later)
|
## How to install [dev]
|
||||||
+ MySQL should work too, but there are no guarantees.
|
|
||||||
- Python 3.9 or higher
|
|
||||||
- PyCharm or similar IDE (optional)
|
|
||||||
|
|
||||||
### Step 1: Preparing Database
|
TBD
|
||||||
|
|
||||||
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,5 +1,5 @@
|
|||||||
[lan]
|
[lan]
|
||||||
name="EZGG LAN"
|
name="EZ LAN"
|
||||||
iteration="0.5"
|
iteration="0.5"
|
||||||
date_from="2024-10-30 15:00:00"
|
date_from="2024-10-30 15:00:00"
|
||||||
date_till="2024-11-01 12:00:00"
|
date_till="2024-11-01 12:00:00"
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
db_password="demo_password"
|
db_password="demo_password"
|
||||||
db_host="127.0.0.1"
|
db_host="127.0.0.1"
|
||||||
db_port=3306
|
db_port=3306
|
||||||
db_name="ezgg_lan_manager"
|
db_name="ez_lan_manager"
|
||||||
|
|
||||||
[mailing]
|
[mailing]
|
||||||
smtp_server=""
|
smtp_server=""
|
||||||
@@ -19,6 +19,12 @@
|
|||||||
username=""
|
username=""
|
||||||
password=""
|
password=""
|
||||||
|
|
||||||
|
[seating]
|
||||||
|
# SeatID -> Category
|
||||||
|
A01 = "NORMAL"
|
||||||
|
A02 = "NORMAL"
|
||||||
|
C01 = "LUXUS"
|
||||||
|
|
||||||
[tickets]
|
[tickets]
|
||||||
[tickets."NORMAL"]
|
[tickets."NORMAL"]
|
||||||
total_tickets=30
|
total_tickets=30
|
||||||
@@ -34,11 +40,5 @@
|
|||||||
additional_info="Berechtigt zur Nutzung eines verbesserten Platzes. Dieser ist mit einer höheren Internet-Bandbreite und einem Sitzkissen ausgestattet."
|
additional_info="Berechtigt zur Nutzung eines verbesserten Platzes. Dieser ist mit einer höheren Internet-Bandbreite und einem Sitzkissen ausgestattet."
|
||||||
is_default=false
|
is_default=false
|
||||||
|
|
||||||
[receipt_printing]
|
|
||||||
host="127.0.0.1"
|
|
||||||
port="5000"
|
|
||||||
order_print_endpoint="print_order"
|
|
||||||
password="Alkohol1"
|
|
||||||
|
|
||||||
[misc]
|
[misc]
|
||||||
dev_mode_active=true # Supresses E-Mail sending
|
dev_mode_active=true # Supresses E-Mail sending
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
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:
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
-- 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 */;
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
-- =====================================================
|
|
||||||
-- Teams
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS `team_members`;
|
|
||||||
DROP TABLE IF EXISTS `teams`;
|
|
||||||
|
|
||||||
|
|
||||||
-- -----------------------------------------------------
|
|
||||||
-- Teams table
|
|
||||||
-- -----------------------------------------------------
|
|
||||||
CREATE TABLE `teams` (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
name VARCHAR(100) NOT NULL,
|
|
||||||
abbreviation VARCHAR(10) NOT NULL,
|
|
||||||
join_password VARCHAR(255) NOT NULL,
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
UNIQUE KEY uq_team_name (name),
|
|
||||||
UNIQUE KEY uq_team_abbr (abbreviation)
|
|
||||||
|
|
||||||
) ENGINE=InnoDB
|
|
||||||
DEFAULT CHARSET=utf8mb4
|
|
||||||
COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
|
|
||||||
-- -----------------------------------------------------
|
|
||||||
-- Team Members (Junction Table)
|
|
||||||
-- -----------------------------------------------------
|
|
||||||
CREATE TABLE `team_members` (
|
|
||||||
team_id INT NOT NULL,
|
|
||||||
user_id INT NOT NULL,
|
|
||||||
|
|
||||||
status ENUM('MEMBER','OFFICER','LEADER')
|
|
||||||
NOT NULL DEFAULT 'MEMBER',
|
|
||||||
|
|
||||||
joined_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
PRIMARY KEY (team_id, user_id),
|
|
||||||
|
|
||||||
CONSTRAINT fk_tm_team
|
|
||||||
FOREIGN KEY (team_id)
|
|
||||||
REFERENCES teams(id)
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
|
|
||||||
CONSTRAINT fk_tm_user
|
|
||||||
FOREIGN KEY (user_id)
|
|
||||||
REFERENCES users(user_id)
|
|
||||||
ON DELETE CASCADE
|
|
||||||
|
|
||||||
) ENGINE=InnoDB
|
|
||||||
DEFAULT CHARSET=utf8mb4
|
|
||||||
COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
|
|
||||||
-- -----------------------------------------------------
|
|
||||||
-- Indexes
|
|
||||||
-- -----------------------------------------------------
|
|
||||||
CREATE INDEX idx_tm_user
|
|
||||||
ON team_members(user_id);
|
|
||||||
|
|
||||||
CREATE INDEX idx_tm_team_status
|
|
||||||
ON team_members(team_id, status);
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
-- =====================================================
|
|
||||||
-- Adds type of participant to tournament
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
ALTER TABLE `tournaments` ADD COLUMN `participant_type` ENUM('PLAYER','TEAM') NOT NULL DEFAULT 'PLAYER' AFTER `created_at`;
|
|
||||||
|
|
||||||
ALTER TABLE `tournament_participants`
|
|
||||||
CHANGE COLUMN `user_id` `user_id` INT(11) NULL AFTER `tournament_id`,
|
|
||||||
ADD COLUMN `team_id` INT(11) NULL AFTER `user_id`,
|
|
||||||
ADD CONSTRAINT `fk_tp_team` FOREIGN KEY (`team_id`) REFERENCES `teams` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
-- Apply this patch after using create_database.sql to extend the schema to support fallback passwords
|
|
||||||
|
|
||||||
ALTER TABLE users
|
|
||||||
ADD COLUMN user_fallback_password VARCHAR(255) DEFAULT NULL
|
|
||||||
AFTER user_password;
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
CREATE DATABASE IF NOT EXISTS `ezgg_lan_manager` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci */;
|
CREATE DATABASE IF NOT EXISTS `ez_lan_manager` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci */;
|
||||||
USE `ezgg_lan_manager`;
|
USE `ez_lan_manager`;
|
||||||
-- MySQL dump 10.13 Distrib 5.7.24, for Linux (x86_64)
|
-- MySQL dump 10.13 Distrib 5.7.24, for Linux (x86_64)
|
||||||
--
|
--
|
||||||
-- Host: 127.0.0.1 Database: ezgg_lan_manager
|
-- Host: 127.0.0.1 Database: ez_lan_manager
|
||||||
-- ------------------------------------------------------
|
-- ------------------------------------------------------
|
||||||
-- Server version 5.5.5-10.11.8-MariaDB-0ubuntu0.24.04.1
|
-- Server version 5.5.5-10.11.8-MariaDB-0ubuntu0.24.04.1
|
||||||
|
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
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');
|
|
||||||
@@ -1,19 +1,20 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from asyncio import get_event_loop
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from rio import App, Theme, Color, Font, ComponentPage, Session
|
from rio import App, Theme, Color, Font, ComponentPage, Session
|
||||||
from from_root import from_root
|
from from_root import from_root
|
||||||
|
|
||||||
from src.ezgg_lan_manager import pages, init_services, LocalDataService
|
from src.ez_lan_manager import pages, init_services
|
||||||
from src.ezgg_lan_manager.helpers.LoggedInGuard import logged_in_guard, not_logged_in_guard, team_guard
|
from src.ez_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.ez_lan_manager.services.DatabaseService import NoDatabaseConnectionError
|
||||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
from src.ez_lan_manager.services.LocalDataService import LocalData
|
||||||
|
from src.ez_lan_manager.types.SessionStorage import SessionStorage
|
||||||
|
|
||||||
logger = logging.getLogger("EzggLanManager")
|
logger = logging.getLogger("EzLanManager")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
theme = Theme.from_colors(
|
theme = Theme.from_colors(
|
||||||
@@ -27,21 +28,16 @@ if __name__ == "__main__":
|
|||||||
corner_radius_small=0,
|
corner_radius_small=0,
|
||||||
corner_radius_medium=0,
|
corner_radius_medium=0,
|
||||||
corner_radius_large=0,
|
corner_radius_large=0,
|
||||||
font=Font(from_root("src/ezgg_lan_manager/assets/fonts/joystix.otf"))
|
font=Font(from_root("src/ez_lan_manager/assets/fonts/joystix.otf"))
|
||||||
)
|
)
|
||||||
default_attachments: list = [LocalData(stored_session_token=None)]
|
default_attachments = [LocalData()]
|
||||||
default_attachments.extend(init_services())
|
default_attachments.extend(init_services())
|
||||||
|
|
||||||
lan_info = default_attachments[3].get_lan_info()
|
lan_info = default_attachments[3].get_lan_info()
|
||||||
|
|
||||||
async def on_session_start(session: Session) -> None:
|
async def on_session_start(session: Session) -> None:
|
||||||
# Use this line to fake being any user without having to log in
|
|
||||||
# session.attach(UserSession(id=uuid4(), user_id=30, is_team_member=True))
|
|
||||||
await session.set_title(lan_info.name)
|
await session.set_title(lan_info.name)
|
||||||
if session[LocalData].stored_session_token:
|
session.attach(SessionStorage())
|
||||||
user_session = session[LocalDataService].verify_token(session[LocalData].stored_session_token)
|
|
||||||
if user_session is not None:
|
|
||||||
session.attach(user_session)
|
|
||||||
|
|
||||||
async def on_app_start(a: App) -> None:
|
async def on_app_start(a: App) -> None:
|
||||||
init_result = await a.default_attachments[4].init_db_pool()
|
init_result = await a.default_attachments[4].init_db_pool()
|
||||||
@@ -50,7 +46,7 @@ if __name__ == "__main__":
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
app = App(
|
app = App(
|
||||||
name="EZGG LAN Manager",
|
name="EZ LAN Manager",
|
||||||
build=pages.BasePage,
|
build=pages.BasePage,
|
||||||
pages=[
|
pages=[
|
||||||
ComponentPage(
|
ComponentPage(
|
||||||
@@ -66,7 +62,7 @@ if __name__ == "__main__":
|
|||||||
ComponentPage(
|
ComponentPage(
|
||||||
name="Overview",
|
name="Overview",
|
||||||
url_segment="overview",
|
url_segment="overview",
|
||||||
build=pages.OverviewPage,
|
build=lambda: pages.PlaceholderPage(placeholder_name="LAN Übersicht"),
|
||||||
),
|
),
|
||||||
ComponentPage(
|
ComponentPage(
|
||||||
name="BuyTicket",
|
name="BuyTicket",
|
||||||
@@ -161,36 +157,10 @@ if __name__ == "__main__":
|
|||||||
build=pages.ManageTournamentsPage,
|
build=pages.ManageTournamentsPage,
|
||||||
guard=team_guard
|
guard=team_guard
|
||||||
),
|
),
|
||||||
ComponentPage(
|
|
||||||
name="AdminNavigationPage",
|
|
||||||
url_segment="admin",
|
|
||||||
build=pages.AdminNavigationPage,
|
|
||||||
guard=team_guard
|
|
||||||
),
|
|
||||||
ComponentPage(
|
ComponentPage(
|
||||||
name="DbErrorPage",
|
name="DbErrorPage",
|
||||||
url_segment="db-error",
|
url_segment="db-error",
|
||||||
build=pages.DbErrorPage,
|
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,
|
theme=theme,
|
||||||
@@ -198,13 +168,13 @@ if __name__ == "__main__":
|
|||||||
default_attachments=default_attachments,
|
default_attachments=default_attachments,
|
||||||
on_session_start=on_session_start,
|
on_session_start=on_session_start,
|
||||||
on_app_start=on_app_start,
|
on_app_start=on_app_start,
|
||||||
icon=from_root("src/ezgg_lan_manager/assets/img/favicon.png"),
|
icon=from_root("src/ez_lan_manager/assets/img/favicon.png"),
|
||||||
meta_tags={
|
meta_tags={
|
||||||
"robots": "INDEX,FOLLOW",
|
"robots": "INDEX,FOLLOW",
|
||||||
"description": f"Info und Verwaltungs-Seite der LAN Party '{lan_info.name} - {lan_info.iteration}'.",
|
"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}'.",
|
"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, "
|
"keywords": "Gaming, Clan, Guild, Verein, Club, Einfach, Zocken, Gesellschaft, Videospiele, "
|
||||||
"Videogames, LAN, Party, EZ, EZGG, LAN, Manager",
|
"Videogames, LAN, Party, EZ, LAN, Manager",
|
||||||
"author": "David Rodenkirchen",
|
"author": "David Rodenkirchen",
|
||||||
"publisher": "EZ GG e.V.",
|
"publisher": "EZ GG e.V.",
|
||||||
"copyright": "EZ GG e.V.",
|
"copyright": "EZ GG e.V.",
|
||||||
@@ -216,7 +186,4 @@ if __name__ == "__main__":
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
sys.exit(app.run_as_web_server(
|
sys.exit(app.run_as_web_server())
|
||||||
host="0.0.0.0",
|
|
||||||
port=8000,
|
|
||||||
))
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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
|
||||||
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,38 @@
|
|||||||
|
from asyncio import sleep
|
||||||
|
|
||||||
|
from rio import Text, Component, TextStyle
|
||||||
|
|
||||||
|
|
||||||
|
class AnimatedText(Component):
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self._display_printing: list[bool] = [False]
|
||||||
|
self.text_comp = Text("")
|
||||||
|
|
||||||
|
async def display_text(self, success: bool, text: str, speed: float = 0.06, font_size: float = 0.9) -> None:
|
||||||
|
if self._display_printing[0]:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self._display_printing[0] = True
|
||||||
|
self.text_comp.text = ""
|
||||||
|
if success:
|
||||||
|
self.text_comp.style = TextStyle(
|
||||||
|
fill=self.session.theme.success_color,
|
||||||
|
font_size=font_size
|
||||||
|
)
|
||||||
|
for c in text:
|
||||||
|
self.text_comp.text = self.text_comp.text + c
|
||||||
|
self.text_comp.force_refresh()
|
||||||
|
await sleep(speed)
|
||||||
|
else:
|
||||||
|
self.text_comp.style = TextStyle(
|
||||||
|
fill=self.session.theme.danger_color,
|
||||||
|
font_size=font_size
|
||||||
|
)
|
||||||
|
for c in text:
|
||||||
|
self.text_comp.text = self.text_comp.text + c
|
||||||
|
self.text_comp.force_refresh()
|
||||||
|
await sleep(speed)
|
||||||
|
self._display_printing[0] = False
|
||||||
|
|
||||||
|
def build(self) -> Component:
|
||||||
|
return self.text_comp
|
||||||
@@ -4,7 +4,7 @@ from decimal import Decimal
|
|||||||
import rio
|
import rio
|
||||||
from rio import Component, Row, Text, IconButton, TextStyle
|
from rio import Component, Row, Text, IconButton, TextStyle
|
||||||
|
|
||||||
from src.ezgg_lan_manager import AccountingService
|
from src.ez_lan_manager import AccountingService
|
||||||
|
|
||||||
MAX_LEN = 24
|
MAX_LEN = 24
|
||||||
|
|
||||||
@@ -3,9 +3,9 @@ from typing import Optional, Callable
|
|||||||
|
|
||||||
from rio import Component, Row, Card, Column, Text, TextStyle, Spacer, PointerEventListener, Button
|
from rio import Component, Row, Card, Column, Text, TextStyle, Spacer, PointerEventListener, Button
|
||||||
|
|
||||||
from src.ezgg_lan_manager.services.CateringService import CateringService
|
from src.ez_lan_manager.services.CateringService import CateringService
|
||||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus
|
from src.ez_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus
|
||||||
from src.ezgg_lan_manager.types.Seat import Seat
|
from src.ez_lan_manager.types.Seat import Seat
|
||||||
|
|
||||||
class CateringManagementOrderDisplayStatusButton(Component):
|
class CateringManagementOrderDisplayStatusButton(Component):
|
||||||
status: CateringOrderStatus
|
status: CateringOrderStatus
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from rio import Component, Row, Text, TextStyle, Color, Rectangle, PointerEventListener
|
from rio import Component, Row, Text, TextStyle, Color, Rectangle, CursorStyle
|
||||||
|
from rio.components.pointer_event_listener import PointerEvent, PointerEventListener
|
||||||
|
|
||||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrderStatus, CateringOrder
|
from src.ez_lan_manager.types.CateringOrder import CateringOrderStatus, CateringOrder
|
||||||
|
|
||||||
MAX_LEN = 24
|
MAX_LEN = 24
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ class CateringOrderItem(Component):
|
|||||||
fill=self.session.theme.primary_color,
|
fill=self.session.theme.primary_color,
|
||||||
hover_fill=self.session.theme.hud_color,
|
hover_fill=self.session.theme.hud_color,
|
||||||
transition_time=0.1,
|
transition_time=0.1,
|
||||||
cursor="pointer"
|
cursor=CursorStyle.POINTER
|
||||||
),
|
),
|
||||||
on_press=lambda _: self.info_modal_cb(self.order),
|
on_press=lambda _: self.info_modal_cb(self.order),
|
||||||
)
|
)
|
||||||
@@ -4,7 +4,7 @@ from typing import Callable
|
|||||||
import rio
|
import rio
|
||||||
from rio import Component, Row, Text, IconButton, TextStyle, Column, Spacer, Card, Color
|
from rio import Component, Row, Text, IconButton, TextStyle, Column, Spacer, Card, Color
|
||||||
|
|
||||||
from src.ezgg_lan_manager import AccountingService
|
from src.ez_lan_manager import AccountingService
|
||||||
|
|
||||||
MAX_LEN = 24
|
MAX_LEN = 24
|
||||||
|
|
||||||
@@ -46,10 +46,10 @@ class CateringSelectionItem(Component):
|
|||||||
Text(AccountingService.make_euro_string_from_decimal(self.article_price),
|
Text(AccountingService.make_euro_string_from_decimal(self.article_price),
|
||||||
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
|
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon="material/add" if self.is_sensitive else "material/do_not_disturb_on_total_silence",
|
icon="material/add",
|
||||||
min_size=2,
|
min_size=2,
|
||||||
color=self.session.theme.success_color if self.is_sensitive else self.session.theme.danger_color,
|
color=self.session.theme.success_color,
|
||||||
style="colored-text",
|
style="plain-text",
|
||||||
on_press=lambda: self.on_add_callback(self.article_id),
|
on_press=lambda: self.on_add_callback(self.article_id),
|
||||||
is_sensitive=self.is_sensitive
|
is_sensitive=self.is_sensitive
|
||||||
),
|
),
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
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,17 +1,15 @@
|
|||||||
import uuid
|
|
||||||
|
|
||||||
from rio import Component, TextStyle, Color, TextInput, Button, Text, Rectangle, Column, Row, Spacer, \
|
from rio import Component, TextStyle, Color, TextInput, Button, Text, Rectangle, Column, Row, Spacer, \
|
||||||
EventHandler, Webview
|
EventHandler
|
||||||
|
|
||||||
from src.ezgg_lan_manager import RefreshService
|
from src.ez_lan_manager.services.LocalDataService import LocalDataService, LocalData
|
||||||
from src.ezgg_lan_manager.services.LocalDataService import LocalDataService, LocalData
|
from src.ez_lan_manager.services.UserService import UserService
|
||||||
from src.ezgg_lan_manager.services.UserService import UserService
|
from src.ez_lan_manager.types.SessionStorage import SessionStorage
|
||||||
from src.ezgg_lan_manager.types.User import User
|
from src.ez_lan_manager.types.User import User
|
||||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
|
||||||
|
|
||||||
|
|
||||||
class LoginBox(Component):
|
class LoginBox(Component):
|
||||||
status_change_cb: EventHandler = None
|
status_change_cb: EventHandler = None
|
||||||
|
TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
|
||||||
user_name_input_text: str = ""
|
user_name_input_text: str = ""
|
||||||
password_input_text: str = ""
|
password_input_text: str = ""
|
||||||
user_name_input_is_valid = True
|
user_name_input_is_valid = True
|
||||||
@@ -29,13 +27,11 @@ class LoginBox(Component):
|
|||||||
self.password_input_is_valid = True
|
self.password_input_is_valid = True
|
||||||
self.login_button_is_loading = False
|
self.login_button_is_loading = False
|
||||||
self.is_account_locked = False
|
self.is_account_locked = False
|
||||||
user_session = UserSession(id=uuid.uuid4(), user_id=user.user_id, is_team_member=user.is_team_member)
|
await self.session[SessionStorage].set_user_id_and_team_member_flag(user.user_id, user.is_team_member)
|
||||||
self.session.attach(user_session)
|
token = self.session[LocalDataService].set_session(self.session[SessionStorage])
|
||||||
token = self.session[LocalDataService].set_session(user_session)
|
|
||||||
self.session[LocalData].stored_session_token = token
|
self.session[LocalData].stored_session_token = token
|
||||||
self.session.attach(self.session[LocalData])
|
self.session.attach(self.session[LocalData])
|
||||||
await self.status_change_cb()
|
self.status_change_cb()
|
||||||
await self.session[RefreshService].trigger_refresh()
|
|
||||||
else:
|
else:
|
||||||
self.user_name_input_is_valid = False
|
self.user_name_input_is_valid = False
|
||||||
self.password_input_is_valid = False
|
self.password_input_is_valid = False
|
||||||
@@ -61,7 +57,7 @@ class LoginBox(Component):
|
|||||||
is_valid=self.password_input_is_valid
|
is_valid=self.password_input_is_valid
|
||||||
)
|
)
|
||||||
login_button = Button(
|
login_button = Button(
|
||||||
Text("LOGIN", fill=Color.from_hex("02dac5"), style=TextStyle(font_size=0.9), justify="center"),
|
Text("LOGIN", style=self.TEXT_STYLE, justify="center"),
|
||||||
shape="rectangle",
|
shape="rectangle",
|
||||||
style="minor",
|
style="minor",
|
||||||
color="secondary",
|
color="secondary",
|
||||||
@@ -69,14 +65,14 @@ class LoginBox(Component):
|
|||||||
on_press=self._on_login_pressed
|
on_press=self._on_login_pressed
|
||||||
)
|
)
|
||||||
register_button = Button(
|
register_button = Button(
|
||||||
Text("REG", fill=Color.from_hex("02dac5"), style=TextStyle(font_size=0.9), justify="center"),
|
Text("REG", style=self.TEXT_STYLE, justify="center"),
|
||||||
shape="rectangle",
|
shape="rectangle",
|
||||||
style="minor",
|
style="minor",
|
||||||
color="secondary",
|
color="secondary",
|
||||||
on_press=lambda: self.session.navigate_to("./register")
|
on_press=lambda: self.session.navigate_to("./register")
|
||||||
)
|
)
|
||||||
forgot_password_button = Button(
|
forgot_password_button = Button(
|
||||||
Text("LST PWD", fill=Color.from_hex("02dac5"), style=TextStyle(font_size=0.9), justify="center"),
|
Text("LST PWD", style=self.TEXT_STYLE, justify="center"),
|
||||||
shape="rectangle",
|
shape="rectangle",
|
||||||
style="minor",
|
style="minor",
|
||||||
color="secondary",
|
color="secondary",
|
||||||
@@ -99,7 +95,7 @@ class LoginBox(Component):
|
|||||||
),
|
),
|
||||||
margin_bottom=0.5
|
margin_bottom=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),
|
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),
|
||||||
spacing=0.4
|
spacing=0.4
|
||||||
),
|
),
|
||||||
fill=Color.TRANSPARENT,
|
fill=Color.TRANSPARENT,
|
||||||
@@ -107,5 +103,5 @@ class LoginBox(Component):
|
|||||||
min_width=12,
|
min_width=12,
|
||||||
align_x=0.5,
|
align_x=0.5,
|
||||||
margin_top=0.3,
|
margin_top=0.3,
|
||||||
margin_bottom=1.5
|
margin_bottom=2
|
||||||
)
|
)
|
||||||
@@ -4,8 +4,8 @@ from typing import Optional
|
|||||||
|
|
||||||
from rio import Component, Column, NumberInput, ThemeContextSwitcher, TextInput, Row, Button, EventHandler
|
from rio import Component, Column, NumberInput, ThemeContextSwitcher, TextInput, Row, Button, EventHandler
|
||||||
|
|
||||||
from src.ezgg_lan_manager.types.Transaction import Transaction
|
from src.ez_lan_manager.types.Transaction import Transaction
|
||||||
from src.ezgg_lan_manager.types.User import User
|
from src.ez_lan_manager.types.User import User
|
||||||
|
|
||||||
|
|
||||||
class NewTransactionForm(Component):
|
class NewTransactionForm(Component):
|
||||||
@@ -46,6 +46,7 @@ class NewTransactionForm(Component):
|
|||||||
label="Betrag",
|
label="Betrag",
|
||||||
suffix_text="€",
|
suffix_text="€",
|
||||||
decimals=2,
|
decimals=2,
|
||||||
|
thousands_separator=".",
|
||||||
margin=1,
|
margin=1,
|
||||||
margin_bottom=0
|
margin_bottom=0
|
||||||
),
|
),
|
||||||
@@ -21,8 +21,8 @@ class NewsPost(Component):
|
|||||||
grow_x=True,
|
grow_x=True,
|
||||||
margin=2,
|
margin=2,
|
||||||
margin_bottom=0,
|
margin_bottom=0,
|
||||||
fill=self.session.theme.background_color,
|
|
||||||
style=TextStyle(
|
style=TextStyle(
|
||||||
|
fill=self.session.theme.background_color,
|
||||||
font_size=1.3
|
font_size=1.3
|
||||||
),
|
),
|
||||||
overflow="ellipsize"
|
overflow="ellipsize"
|
||||||
@@ -31,8 +31,8 @@ class NewsPost(Component):
|
|||||||
self.date,
|
self.date,
|
||||||
margin=2,
|
margin=2,
|
||||||
align_x=1,
|
align_x=1,
|
||||||
fill=self.session.theme.background_color,
|
|
||||||
style=TextStyle(
|
style=TextStyle(
|
||||||
|
fill=self.session.theme.background_color,
|
||||||
font_size=0.6
|
font_size=0.6
|
||||||
),
|
),
|
||||||
overflow="wrap"
|
overflow="wrap"
|
||||||
@@ -44,8 +44,8 @@ class NewsPost(Component):
|
|||||||
margin=2,
|
margin=2,
|
||||||
margin_top=0,
|
margin_top=0,
|
||||||
margin_bottom=0,
|
margin_bottom=0,
|
||||||
fill=self.session.theme.background_color,
|
|
||||||
style=TextStyle(
|
style=TextStyle(
|
||||||
|
fill=self.session.theme.background_color,
|
||||||
font_size=0.8
|
font_size=0.8
|
||||||
),
|
),
|
||||||
overflow="ellipsize"
|
overflow="ellipsize"
|
||||||
@@ -53,7 +53,9 @@ class NewsPost(Component):
|
|||||||
Text(
|
Text(
|
||||||
self.text,
|
self.text,
|
||||||
margin=2,
|
margin=2,
|
||||||
fill=self.session.theme.background_color,
|
style=TextStyle(
|
||||||
|
fill=self.session.theme.background_color
|
||||||
|
),
|
||||||
overflow="wrap"
|
overflow="wrap"
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
@@ -63,8 +65,8 @@ class NewsPost(Component):
|
|||||||
margin=2,
|
margin=2,
|
||||||
margin_top=0,
|
margin_top=0,
|
||||||
margin_bottom=1,
|
margin_bottom=1,
|
||||||
fill=self.session.theme.background_color,
|
|
||||||
style=TextStyle(
|
style=TextStyle(
|
||||||
|
fill=self.session.theme.background_color,
|
||||||
font_size=0.5,
|
font_size=0.5,
|
||||||
italic=True
|
italic=True
|
||||||
),
|
),
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
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,10 +1,7 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Optional, Callable
|
from typing import Optional, Callable
|
||||||
|
|
||||||
from rio import Component, Column, Text, TextStyle, Button, Spacer, event
|
from rio import Component, Column, Text, TextStyle, Button, Spacer
|
||||||
|
|
||||||
from src.ezgg_lan_manager import TicketingService
|
|
||||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
|
||||||
|
|
||||||
|
|
||||||
class SeatingPlanInfoBox(Component):
|
class SeatingPlanInfoBox(Component):
|
||||||
@@ -15,38 +12,8 @@ class SeatingPlanInfoBox(Component):
|
|||||||
seat_occupant: Optional[str] = None
|
seat_occupant: Optional[str] = None
|
||||||
seat_price: Decimal = Decimal("0")
|
seat_price: Decimal = Decimal("0")
|
||||||
is_blocked: bool = False
|
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:
|
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:
|
if not self.show:
|
||||||
return Spacer()
|
return Spacer()
|
||||||
if self.is_blocked:
|
if self.is_blocked:
|
||||||
@@ -69,7 +36,7 @@ class SeatingPlanInfoBox(Component):
|
|||||||
style=TextStyle(fill=self.session.theme.neutral_color), overflow="wrap", justify="center"),
|
style=TextStyle(fill=self.session.theme.neutral_color), overflow="wrap", justify="center"),
|
||||||
Button(
|
Button(
|
||||||
Text(
|
Text(
|
||||||
text=self.booking_button_text,
|
f"Buchen",
|
||||||
margin=1,
|
margin=1,
|
||||||
style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.1),
|
style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.1),
|
||||||
overflow="wrap",
|
overflow="wrap",
|
||||||
@@ -81,10 +48,7 @@ class SeatingPlanInfoBox(Component):
|
|||||||
margin=1,
|
margin=1,
|
||||||
grow_y=False,
|
grow_y=False,
|
||||||
is_sensitive=not self.is_booking_blocked,
|
is_sensitive=not self.is_booking_blocked,
|
||||||
on_press=self.purchase_clicked
|
on_press=self.purchase_cb
|
||||||
) 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
|
min_height=10
|
||||||
)
|
)
|
||||||
@@ -1,61 +1,43 @@
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from rio import Component, Text, Icon, TextStyle, Rectangle, Spacer, Color, PointerEventListener, Column, Row, PointerEvent, Tooltip
|
from rio import Component, Text, Icon, TextStyle, Rectangle, Spacer, Color, PointerEventListener, Column
|
||||||
from typing import Optional, Callable, Literal
|
from typing import Optional, Callable
|
||||||
|
|
||||||
from src.ezgg_lan_manager.types.Seat import Seat
|
from src.ez_lan_manager.types.Seat import Seat
|
||||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
from src.ez_lan_manager.types.SessionStorage import SessionStorage
|
||||||
|
|
||||||
|
|
||||||
class SeatPixel(Component):
|
class SeatPixel(Component):
|
||||||
seat_id: str
|
seat_id: str
|
||||||
on_press_cb: Callable
|
on_press_cb: Callable
|
||||||
seat: Seat
|
seat: Seat
|
||||||
seat_orientation: Literal["top", "bottom"]
|
|
||||||
|
|
||||||
def determine_color(self) -> Color:
|
def determine_color(self) -> Color:
|
||||||
try:
|
if self.seat.user is not None and self.seat.user.user_id == self.session[SessionStorage].user_id:
|
||||||
user_id = self.session[UserSession].user_id
|
|
||||||
except KeyError:
|
|
||||||
user_id = None
|
|
||||||
if self.seat.user is not None and self.seat.user.user_id == user_id:
|
|
||||||
return Color.from_hex("800080")
|
return Color.from_hex("800080")
|
||||||
elif self.seat.is_blocked or self.seat.user is not None:
|
elif self.seat.is_blocked or self.seat.user is not None:
|
||||||
return self.session.theme.danger_color
|
return self.session.theme.danger_color
|
||||||
return self.session.theme.success_color
|
return self.session.theme.success_color
|
||||||
|
|
||||||
def build(self) -> Component:
|
def build(self) -> Component:
|
||||||
text = Text(f"{self.seat_id}", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9), align_x=0.5, selectable=False)
|
return PointerEventListener(
|
||||||
rec = Rectangle(
|
content=Rectangle(
|
||||||
content=Row(text),
|
content=Column(
|
||||||
min_width=1,
|
Text(f"{self.seat_id}", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.7), align_x=0.5, selectable=False),
|
||||||
min_height=1,
|
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")
|
||||||
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),
|
min_width=1,
|
||||||
)
|
min_height=1,
|
||||||
else:
|
fill=self.determine_color(),
|
||||||
return PointerEventListener(
|
hover_stroke_width = 0.1,
|
||||||
content=rec,
|
grow_x=True,
|
||||||
on_press=partial(self.on_press_cb, self.seat_id),
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
class TextPixel(Component):
|
class TextPixel(Component):
|
||||||
text: Optional[str] = None
|
text: Optional[str] = None
|
||||||
@@ -76,14 +58,13 @@ class TextPixel(Component):
|
|||||||
fill=self.session.theme.primary_color,
|
fill=self.session.theme.primary_color,
|
||||||
stroke_width=0.0 if self.no_outline else 0.1,
|
stroke_width=0.0 if self.no_outline else 0.1,
|
||||||
stroke_color=self.session.theme.neutral_color,
|
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_x=True,
|
||||||
grow_y=True,
|
grow_y=True,
|
||||||
hover_fill=None,
|
hover_fill=None,
|
||||||
ripple=True
|
ripple=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class WallPixel(Component):
|
class WallPixel(Component):
|
||||||
def build(self) -> Component:
|
def build(self) -> Component:
|
||||||
return Rectangle(
|
return Rectangle(
|
||||||
@@ -94,7 +75,6 @@ class WallPixel(Component):
|
|||||||
grow_y=True,
|
grow_y=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DebugPixel(Component):
|
class DebugPixel(Component):
|
||||||
def build(self) -> Component:
|
def build(self) -> Component:
|
||||||
return Rectangle(
|
return Rectangle(
|
||||||
@@ -102,15 +82,14 @@ class DebugPixel(Component):
|
|||||||
min_width=1,
|
min_width=1,
|
||||||
min_height=1,
|
min_height=1,
|
||||||
fill=self.session.theme.success_color,
|
fill=self.session.theme.success_color,
|
||||||
hover_stroke_color=self.session.theme.hud_color,
|
hover_stroke_color = self.session.theme.hud_color,
|
||||||
hover_stroke_width=0.1,
|
hover_stroke_width = 0.1,
|
||||||
grow_x=True,
|
grow_x=True,
|
||||||
grow_y=True,
|
grow_y=True,
|
||||||
hover_fill=self.session.theme.secondary_color,
|
hover_fill=self.session.theme.secondary_color,
|
||||||
transition_time=0.1
|
transition_time=0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class InvisiblePixel(Component):
|
class InvisiblePixel(Component):
|
||||||
def build(self) -> Component:
|
def build(self) -> Component:
|
||||||
return Rectangle(
|
return Rectangle(
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
from asyncio import sleep, create_task
|
from asyncio import sleep, create_task
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from rio import Component, Column, Text, TextStyle, Button, Row, ScrollContainer, Spacer, Popup, Table, event, Card
|
import rio
|
||||||
|
from rio import Component, Column, Text, TextStyle, Button, Row, ScrollContainer, Spacer, Popup, Table
|
||||||
|
|
||||||
from src.ezgg_lan_manager.components.CateringCartItem import CateringCartItem
|
from src.ez_lan_manager.components.CateringCartItem import CateringCartItem
|
||||||
from src.ezgg_lan_manager.components.CateringOrderItem import CateringOrderItem
|
from src.ez_lan_manager.components.CateringOrderItem import CateringOrderItem
|
||||||
from src.ezgg_lan_manager.services.AccountingService import AccountingService
|
from src.ez_lan_manager.services.AccountingService import AccountingService
|
||||||
from src.ezgg_lan_manager.services.CateringService import CateringService, CateringError, CateringErrorType
|
from src.ez_lan_manager.services.CateringService import CateringService, CateringError, CateringErrorType
|
||||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringMenuItemsWithAmount
|
from src.ez_lan_manager.types.CateringOrder import CateringOrder, CateringMenuItemsWithAmount
|
||||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
from src.ez_lan_manager.types.SessionStorage import SessionStorage
|
||||||
|
|
||||||
POPUP_CLOSE_TIMEOUT_SECONDS = 3
|
POPUP_CLOSE_TIMEOUT_SECONDS = 3
|
||||||
|
|
||||||
@@ -21,23 +21,13 @@ class ShoppingCartAndOrders(Component):
|
|||||||
popup_is_shown: bool = False
|
popup_is_shown: bool = False
|
||||||
popup_is_error: bool = True
|
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:
|
async def switch(self) -> None:
|
||||||
self.show_cart = not self.show_cart
|
self.show_cart = not self.show_cart
|
||||||
user_id = self._get_user_id()
|
self.orders = await self.session[CateringService].get_orders_for_user(self.session[SessionStorage].user_id)
|
||||||
if user_id is not None:
|
|
||||||
self.orders = await self.session[CateringService].get_orders_for_user(user_id)
|
|
||||||
|
|
||||||
async def on_remove_item(self, list_id: int) -> None:
|
async def on_remove_item(self, list_id: int) -> None:
|
||||||
catering_service = self.session[CateringService]
|
catering_service = self.session[CateringService]
|
||||||
user_id = self._get_user_id()
|
user_id = self.session[SessionStorage].user_id
|
||||||
if user_id is None:
|
|
||||||
return
|
|
||||||
cart = catering_service.get_cart(user_id)
|
cart = catering_service.get_cart(user_id)
|
||||||
try:
|
try:
|
||||||
cart.pop(list_id)
|
cart.pop(list_id)
|
||||||
@@ -47,16 +37,13 @@ class ShoppingCartAndOrders(Component):
|
|||||||
self.force_refresh()
|
self.force_refresh()
|
||||||
|
|
||||||
async def on_empty_cart_pressed(self) -> None:
|
async def on_empty_cart_pressed(self) -> None:
|
||||||
user_id = self._get_user_id()
|
self.session[CateringService].save_cart(self.session[SessionStorage].user_id, [])
|
||||||
if user_id is None:
|
|
||||||
return
|
|
||||||
self.session[CateringService].save_cart(user_id, [])
|
|
||||||
self.force_refresh()
|
self.force_refresh()
|
||||||
|
|
||||||
async def on_add_item(self, article_id: int) -> None:
|
async def on_add_item(self, article_id: int) -> None:
|
||||||
catering_service = self.session[CateringService]
|
catering_service = self.session[CateringService]
|
||||||
user_id = self._get_user_id()
|
user_id = self.session[SessionStorage].user_id
|
||||||
if user_id is None:
|
if not user_id:
|
||||||
return
|
return
|
||||||
cart = catering_service.get_cart(user_id)
|
cart = catering_service.get_cart(user_id)
|
||||||
item_to_add = await catering_service.get_menu_item_by_id(article_id)
|
item_to_add = await catering_service.get_menu_item_by_id(article_id)
|
||||||
@@ -77,9 +64,7 @@ class ShoppingCartAndOrders(Component):
|
|||||||
self.order_button_loading = True
|
self.order_button_loading = True
|
||||||
self.force_refresh()
|
self.force_refresh()
|
||||||
|
|
||||||
user_id = self._get_user_id()
|
user_id = self.session[SessionStorage].user_id
|
||||||
if user_id is None:
|
|
||||||
return
|
|
||||||
cart = self.session[CateringService].get_cart(user_id)
|
cart = self.session[CateringService].get_cart(user_id)
|
||||||
show_popup_task = None
|
show_popup_task = None
|
||||||
if len(cart) < 1:
|
if len(cart) < 1:
|
||||||
@@ -100,14 +85,13 @@ class ShoppingCartAndOrders(Component):
|
|||||||
show_popup_task = create_task(self.show_popup("Guthaben nicht ausreichend", True))
|
show_popup_task = create_task(self.show_popup("Guthaben nicht ausreichend", True))
|
||||||
else:
|
else:
|
||||||
show_popup_task = create_task(self.show_popup("Unbekannter Fehler", True))
|
show_popup_task = create_task(self.show_popup("Unbekannter Fehler", True))
|
||||||
else:
|
self.session[CateringService].save_cart(self.session[SessionStorage].user_id, [])
|
||||||
self.session[CateringService].save_cart(user_id, [])
|
|
||||||
self.order_button_loading = False
|
self.order_button_loading = False
|
||||||
if not show_popup_task:
|
if not show_popup_task:
|
||||||
show_popup_task = create_task(self.show_popup("Bestellung erfolgreich aufgegeben!", False))
|
show_popup_task = create_task(self.show_popup("Bestellung erfolgreich aufgegeben!", False))
|
||||||
|
|
||||||
async def _create_order_info_modal(self, order: CateringOrder) -> None:
|
async def _create_order_info_modal(self, order: CateringOrder) -> None:
|
||||||
def build_dialog_content() -> Component:
|
def build_dialog_content() -> rio.Component:
|
||||||
# @todo: rio 0.10.8 did not have the ability to align the columns, check back in a future version
|
# @todo: rio 0.10.8 did not have the ability to align the columns, check back in a future version
|
||||||
table = Table(
|
table = Table(
|
||||||
{
|
{
|
||||||
@@ -117,9 +101,9 @@ class ShoppingCartAndOrders(Component):
|
|||||||
},
|
},
|
||||||
show_row_numbers=False
|
show_row_numbers=False
|
||||||
)
|
)
|
||||||
return Card(
|
return rio.Card(
|
||||||
Column(
|
rio.Column(
|
||||||
Text(
|
rio.Text(
|
||||||
f"Deine Bestellung ({order.order_id})",
|
f"Deine Bestellung ({order.order_id})",
|
||||||
align_x=0.5,
|
align_x=0.5,
|
||||||
margin_bottom=0.5
|
margin_bottom=0.5
|
||||||
@@ -140,20 +124,14 @@ class ShoppingCartAndOrders(Component):
|
|||||||
dialog = await self.session.show_custom_dialog(
|
dialog = await self.session.show_custom_dialog(
|
||||||
build=build_dialog_content,
|
build=build_dialog_content,
|
||||||
modal=True,
|
modal=True,
|
||||||
user_closable=True,
|
user_closeable=True,
|
||||||
)
|
)
|
||||||
await dialog.wait_for_close()
|
await dialog.wait_for_close()
|
||||||
|
|
||||||
def _get_user_id(self) -> Optional[int]:
|
def build(self) -> rio.Component:
|
||||||
try:
|
user_id = self.session[SessionStorage].user_id
|
||||||
return self.session[UserSession].user_id
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def build(self) -> Component:
|
|
||||||
user_id = self._get_user_id()
|
|
||||||
catering_service = self.session[CateringService]
|
catering_service = self.session[CateringService]
|
||||||
cart = catering_service.get_cart(user_id) if user_id is not None else []
|
cart = catering_service.get_cart(user_id)
|
||||||
if self.show_cart:
|
if self.show_cart:
|
||||||
cart_container = ScrollContainer(
|
cart_container = ScrollContainer(
|
||||||
content=Column(
|
content=Column(
|
||||||
@@ -171,6 +149,7 @@ class ShoppingCartAndOrders(Component):
|
|||||||
margin=1
|
margin=1
|
||||||
)
|
)
|
||||||
return Column(
|
return Column(
|
||||||
|
cart_container,
|
||||||
Popup(
|
Popup(
|
||||||
anchor=cart_container,
|
anchor=cart_container,
|
||||||
content=Text(self.popup_message, style=TextStyle(fill=self.session.theme.danger_color if self.popup_is_error else self.session.theme.success_color), overflow="wrap", margin=2, justify="center", min_width=20),
|
content=Text(self.popup_message, style=TextStyle(fill=self.session.theme.danger_color if self.popup_is_error else self.session.theme.success_color), overflow="wrap", margin=2, justify="center", min_width=20),
|
||||||
@@ -2,11 +2,12 @@ from functools import partial
|
|||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import rio
|
||||||
from rio import Component, Card, Column, Text, Row, Button, TextStyle, ProgressBar, event, Spacer
|
from rio import Component, Card, Column, Text, Row, Button, TextStyle, ProgressBar, event, Spacer
|
||||||
|
|
||||||
from src.ezgg_lan_manager import TicketingService
|
from src.ez_lan_manager import TicketingService
|
||||||
from src.ezgg_lan_manager.services.AccountingService import AccountingService
|
from src.ez_lan_manager.services.AccountingService import AccountingService
|
||||||
from src.ezgg_lan_manager.types.Ticket import Ticket
|
from src.ez_lan_manager.types.Ticket import Ticket
|
||||||
|
|
||||||
|
|
||||||
class TicketBuyCard(Component):
|
class TicketBuyCard(Component):
|
||||||
@@ -21,10 +22,10 @@ class TicketBuyCard(Component):
|
|||||||
available_tickets: int = 0
|
available_tickets: int = 0
|
||||||
|
|
||||||
@event.on_populate
|
@event.on_populate
|
||||||
async def on_populate(self) -> None:
|
async def async_init(self) -> None:
|
||||||
self.available_tickets = await self.session[TicketingService].get_available_tickets_for_category(self.category)
|
self.available_tickets = await self.session[TicketingService].get_available_tickets_for_category(self.category)
|
||||||
|
|
||||||
def build(self) -> Component:
|
def build(self) -> rio.Component:
|
||||||
ticket_description_style = TextStyle(
|
ticket_description_style = TextStyle(
|
||||||
fill=self.session.theme.neutral_color,
|
fill=self.session.theme.neutral_color,
|
||||||
font_size=1.2,
|
font_size=1.2,
|
||||||
@@ -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, \
|
from rio import Component, Column, Button, Color, TextStyle, Text, TextInput, Row, Image, event, Spacer, DateInput, \
|
||||||
TextInputChangeEvent, NoFileSelectedError
|
TextInputChangeEvent, NoFileSelectedError
|
||||||
|
|
||||||
from src.ezgg_lan_manager.services.UserService import UserService, NameNotAllowedError
|
from src.ez_lan_manager.services.UserService import UserService, NameNotAllowedError
|
||||||
from src.ezgg_lan_manager.services.ConfigurationService import ConfigurationService
|
from src.ez_lan_manager.services.ConfigurationService import ConfigurationService
|
||||||
from src.ezgg_lan_manager.types.User import User
|
from src.ez_lan_manager.types.SessionStorage import SessionStorage
|
||||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
from src.ez_lan_manager.types.User import User
|
||||||
|
|
||||||
|
|
||||||
class UserEditForm(Component):
|
class UserEditForm(Component):
|
||||||
@@ -35,13 +35,8 @@ class UserEditForm(Component):
|
|||||||
async def on_populate(self) -> None:
|
async def on_populate(self) -> None:
|
||||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Profil bearbeiten")
|
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Profil bearbeiten")
|
||||||
if self.is_own_profile:
|
if self.is_own_profile:
|
||||||
try:
|
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||||
user_id = self.session[UserSession].user_id
|
self.profile_picture = await self.session[UserService].get_profile_picture(self.user.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:
|
else:
|
||||||
self.profile_picture = await self.session[UserService].get_profile_picture(self.user.user_id)
|
self.profile_picture = await self.session[UserService].get_profile_picture(self.user.user_id)
|
||||||
|
|
||||||
@@ -127,7 +122,7 @@ class UserEditForm(Component):
|
|||||||
|
|
||||||
def build(self) -> Component:
|
def build(self) -> Component:
|
||||||
pfp_image_container = Image(
|
pfp_image_container = Image(
|
||||||
from_root("src/ezgg_lan_manager/assets/img/anon_pfp.png") if self.profile_picture is None else self.profile_picture,
|
from_root("src/ez_lan_manager/assets/img/anon_pfp.png") if self.profile_picture is None else self.profile_picture,
|
||||||
align_x=0.5,
|
align_x=0.5,
|
||||||
min_width=10,
|
min_width=10,
|
||||||
min_height=10,
|
min_height=10,
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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)
|
||||||
@@ -4,17 +4,16 @@ from decimal import Decimal
|
|||||||
|
|
||||||
from rio import Component, TextStyle, Color, Button, Text, Rectangle, Column, Row, Spacer, Link, event, EventHandler
|
from rio import Component, TextStyle, Color, Button, Text, Rectangle, Column, Row, Spacer, Link, event, EventHandler
|
||||||
|
|
||||||
from src.ezgg_lan_manager.components.UserInfoBoxButton import UserInfoBoxButton
|
from src.ez_lan_manager.components.UserInfoBoxButton import UserInfoBoxButton
|
||||||
from src.ezgg_lan_manager.services.LocalDataService import LocalData, LocalDataService
|
from src.ez_lan_manager.services.LocalDataService import LocalData, LocalDataService
|
||||||
from src.ezgg_lan_manager.services.RefreshService import RefreshService
|
from src.ez_lan_manager.services.UserService import UserService
|
||||||
from src.ezgg_lan_manager.services.UserService import UserService
|
from src.ez_lan_manager.services.AccountingService import AccountingService
|
||||||
from src.ezgg_lan_manager.services.AccountingService import AccountingService
|
from src.ez_lan_manager.services.TicketingService import TicketingService
|
||||||
from src.ezgg_lan_manager.services.TicketingService import TicketingService
|
from src.ez_lan_manager.services.SeatingService import SeatingService
|
||||||
from src.ezgg_lan_manager.services.SeatingService import SeatingService
|
from src.ez_lan_manager.types.Seat import Seat
|
||||||
from src.ezgg_lan_manager.types.Seat import Seat
|
from src.ez_lan_manager.types.Ticket import Ticket
|
||||||
from src.ezgg_lan_manager.types.Ticket import Ticket
|
from src.ez_lan_manager.types.User import User
|
||||||
from src.ezgg_lan_manager.types.User import User
|
from src.ez_lan_manager.types.SessionStorage import SessionStorage
|
||||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
|
||||||
|
|
||||||
|
|
||||||
class StatusButton(Component):
|
class StatusButton(Component):
|
||||||
@@ -42,7 +41,6 @@ class StatusButton(Component):
|
|||||||
|
|
||||||
|
|
||||||
class UserInfoBox(Component):
|
class UserInfoBox(Component):
|
||||||
user_id: int
|
|
||||||
status_change_cb: EventHandler = None
|
status_change_cb: EventHandler = None
|
||||||
TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
|
TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
|
||||||
user: Optional[User] = None
|
user: Optional[User] = None
|
||||||
@@ -55,28 +53,29 @@ class UserInfoBox(Component):
|
|||||||
return choice(["Guten Tacho", "Tuten Gag", "Servus", "Moinjour", "Hallöchen", "Heyho", "Moinsen"])
|
return choice(["Guten Tacho", "Tuten Gag", "Servus", "Moinjour", "Hallöchen", "Heyho", "Moinsen"])
|
||||||
|
|
||||||
async def logout(self) -> None:
|
async def logout(self) -> None:
|
||||||
self.session.detach(UserSession)
|
await self.session[SessionStorage].clear()
|
||||||
self.user = None
|
self.user = None
|
||||||
self.session[LocalDataService].del_session(self.session[LocalData].stored_session_token)
|
self.session[LocalDataService].del_session(self.session[LocalData].stored_session_token)
|
||||||
self.session[LocalData].stored_session_token = None
|
self.session[LocalData].stored_session_token = None
|
||||||
self.session.attach(self.session[LocalData])
|
self.session.attach(self.session[LocalData])
|
||||||
if self.status_change_cb is not None:
|
self.status_change_cb()
|
||||||
await self.status_change_cb()
|
self.session.navigate_to("/")
|
||||||
await self.session[RefreshService].trigger_refresh()
|
|
||||||
self.session.navigate_to("")
|
|
||||||
|
|
||||||
@event.on_populate
|
@event.on_populate
|
||||||
async def async_init(self) -> None:
|
async def async_init(self) -> None:
|
||||||
self.user = await self.session[UserService].get_user(self.user_id)
|
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_balance = await self.session[AccountingService].get_balance(self.user.user_id)
|
self.user_balance = await self.session[AccountingService].get_balance(self.user.user_id)
|
||||||
self.user_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id)
|
self.user_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id)
|
||||||
self.user_seat = await self.session[SeatingService].get_user_seat(self.user.user_id)
|
self.user_seat = await self.session[SeatingService].get_user_seat(self.user.user_id)
|
||||||
self.session[AccountingService].add_update_hook(self.update)
|
|
||||||
|
|
||||||
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:
|
def build(self) -> Component:
|
||||||
if not self.user:
|
if not self.user:
|
||||||
@@ -116,5 +115,5 @@ class UserInfoBox(Component):
|
|||||||
min_width=12,
|
min_width=12,
|
||||||
align_x=0.5,
|
align_x=0.5,
|
||||||
margin_top=0.3,
|
margin_top=0.3,
|
||||||
margin_bottom=1.5
|
margin_bottom=2
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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("./")
|
||||||
@@ -5,9 +5,9 @@ from decimal import Decimal
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from src.ezgg_lan_manager import init_services
|
from src.ez_lan_manager import init_services
|
||||||
from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItemCategory
|
from src.ez_lan_manager.types.CateringMenuItem import CateringMenuItemCategory
|
||||||
from src.ezgg_lan_manager.types.News import News
|
from src.ez_lan_manager.types.News import News
|
||||||
|
|
||||||
DEMO_USERS = [
|
DEMO_USERS = [
|
||||||
{"user_name": "manfred", "user_mail": "manfred@demomail.com", "password_clear_text": "manfred"}, # Gast
|
{"user_name": "manfred", "user_mail": "manfred@demomail.com", "password_clear_text": "manfred"}, # Gast
|
||||||
@@ -185,9 +185,9 @@ async def run() -> None:
|
|||||||
|
|
||||||
await news_service.add_news(News(
|
await news_service.add_news(News(
|
||||||
news_id=None,
|
news_id=None,
|
||||||
title="Der EZGG LAN Manager",
|
title="Der EZ LAN Manager",
|
||||||
subtitle="Eine Software des EZ GG e.V.",
|
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 "
|
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 "
|
"Parties des EZ GG e.V.'s zu organisieren. Wer Fehler findet darf sie behalten. (Oder er meldet "
|
||||||
"sie)",
|
"sie)",
|
||||||
author=user,
|
author=user,
|
||||||
@@ -1,39 +1,28 @@
|
|||||||
from decimal import Decimal
|
from functools import partial
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from rio import Column, Component, event, Text, TextStyle, Button, Color, Revealer, Row, ProgressCircle, Link, Image
|
from rio import Column, Component, event, Text, TextStyle, Button, Color, Revealer, Row, ProgressCircle, Link
|
||||||
|
|
||||||
from src.ezgg_lan_manager import ConfigurationService, UserService, AccountingService
|
from src.ez_lan_manager import ConfigurationService, UserService, AccountingService
|
||||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||||
from src.ezgg_lan_manager.types.Transaction import Transaction
|
from src.ez_lan_manager.types.SessionStorage import SessionStorage
|
||||||
from src.ezgg_lan_manager.types.User import User
|
from src.ez_lan_manager.types.Transaction import Transaction
|
||||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
from src.ez_lan_manager.types.User import User
|
||||||
|
|
||||||
|
|
||||||
class AccountPage(Component):
|
class AccountPage(Component):
|
||||||
user: Optional[User] = None
|
user: Optional[User] = None
|
||||||
balance: Optional[Decimal] = None
|
balance: Optional[int] = None
|
||||||
transaction_history: list[Transaction] = list()
|
transaction_history: list[Transaction] = list()
|
||||||
payment_qr_image: bytes = None
|
|
||||||
banking_info_revealer_open: bool = False
|
banking_info_revealer_open: bool = False
|
||||||
paypal_info_revealer_open: bool = False
|
paypal_info_revealer_open: bool = False
|
||||||
|
|
||||||
@event.on_populate
|
@event.on_populate
|
||||||
async def on_populate(self) -> None:
|
async def on_populate(self) -> None:
|
||||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Guthabenkonto")
|
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Guthabenkonto")
|
||||||
try:
|
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||||
user_id = self.session[UserSession].user_id
|
self.balance = await self.session[AccountingService].get_balance(self.user.user_id)
|
||||||
except KeyError:
|
self.transaction_history = await self.session[AccountingService].get_transaction_history(self.user.user_id)
|
||||||
pass
|
|
||||||
else:
|
|
||||||
self.user = await self.session[UserService].get_user(user_id)
|
|
||||||
self.balance = await self.session[AccountingService].get_balance(user_id)
|
|
||||||
self.transaction_history = await self.session[AccountingService].get_transaction_history(user_id)
|
|
||||||
self.payment_qr_image = self.session[AccountingService].make_payment_qr_image(
|
|
||||||
"Einfach Zocken Gaming Gesellschaft",
|
|
||||||
"GENODE51BIK",
|
|
||||||
"DE47517624340019856607",
|
|
||||||
f"AUFLADUNG - {self.user.user_id} - {self.user.user_name}")
|
|
||||||
|
|
||||||
async def _on_banking_info_press(self) -> None:
|
async def _on_banking_info_press(self) -> None:
|
||||||
self.banking_info_revealer_open = not self.banking_info_revealer_open
|
self.banking_info_revealer_open = not self.banking_info_revealer_open
|
||||||
@@ -42,7 +31,7 @@ class AccountPage(Component):
|
|||||||
self.paypal_info_revealer_open = not self.paypal_info_revealer_open
|
self.paypal_info_revealer_open = not self.paypal_info_revealer_open
|
||||||
|
|
||||||
def build(self) -> Component:
|
def build(self) -> Component:
|
||||||
if not self.user or not self.payment_qr_image:
|
if not self.user and not self.balance:
|
||||||
return Column(
|
return Column(
|
||||||
MainViewContentBox(
|
MainViewContentBox(
|
||||||
ProgressCircle(
|
ProgressCircle(
|
||||||
@@ -91,10 +80,6 @@ class AccountPage(Component):
|
|||||||
margin=0,
|
margin=0,
|
||||||
margin_bottom=1,
|
margin_bottom=1,
|
||||||
align_x=0.5
|
align_x=0.5
|
||||||
),
|
|
||||||
Image(self.payment_qr_image,
|
|
||||||
min_width=20,
|
|
||||||
min_height=20
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
margin=2,
|
margin=2,
|
||||||
@@ -233,20 +218,19 @@ class AccountPage(Component):
|
|||||||
on_press=self._on_paypal_info_press
|
on_press=self._on_paypal_info_press
|
||||||
),
|
),
|
||||||
paypal_info_revealer,
|
paypal_info_revealer,
|
||||||
# Disabled because people did not understand the fee's and kept charging 24.03 € to their accounts
|
Link(
|
||||||
# Link(
|
content=Button(
|
||||||
# content=Button(
|
content=Text("PAYPAL (3% Gebühr - Gewerblich)", style=TextStyle(fill=Color.from_hex("121212"), font_size=0.8), justify="center"),
|
||||||
# content=Text("PAYPAL (3% Gebühr - Gewerblich)", style=TextStyle(fill=Color.from_hex("121212"), font_size=0.8), justify="center"),
|
shape="rectangle",
|
||||||
# shape="rectangle",
|
style="major",
|
||||||
# style="major",
|
color="secondary",
|
||||||
# color="secondary",
|
grow_x=True,
|
||||||
# grow_x=True,
|
margin=2,
|
||||||
# margin=2,
|
margin_top=0
|
||||||
# margin_top=0
|
),
|
||||||
# ),
|
target_url="https://www.paypal.com/ncp/payment/89YMGVZ4S33RS",
|
||||||
# target_url="https://www.paypal.com/ncp/payment/89YMGVZ4S33RS",
|
open_in_new_tab=True
|
||||||
# open_in_new_tab=True
|
)
|
||||||
# )
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
MainViewContentBox(
|
MainViewContentBox(
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
@@ -2,14 +2,14 @@ from typing import Optional
|
|||||||
|
|
||||||
from rio import Text, Column, TextStyle, Component, event, Button, Popup
|
from rio import Text, Column, TextStyle, Component, event, Button, Popup
|
||||||
|
|
||||||
from src.ezgg_lan_manager import ConfigurationService, UserService, TicketingService, RefreshService
|
from src.ez_lan_manager import ConfigurationService, UserService, TicketingService
|
||||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||||
from src.ezgg_lan_manager.components.TicketBuyCard import TicketBuyCard
|
from src.ez_lan_manager.components.TicketBuyCard import TicketBuyCard
|
||||||
from src.ezgg_lan_manager.services.AccountingService import InsufficientFundsError
|
from src.ez_lan_manager.services.AccountingService import InsufficientFundsError
|
||||||
from src.ezgg_lan_manager.services.TicketingService import TicketNotAvailableError, UserAlreadyHasTicketError
|
from src.ez_lan_manager.services.TicketingService import TicketNotAvailableError, UserAlreadyHasTicketError
|
||||||
from src.ezgg_lan_manager.types.Ticket import Ticket
|
from src.ez_lan_manager.types.SessionStorage import SessionStorage
|
||||||
from src.ezgg_lan_manager.types.User import User
|
from src.ez_lan_manager.types.Ticket import Ticket
|
||||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
from src.ez_lan_manager.types.User import User
|
||||||
|
|
||||||
|
|
||||||
class BuyTicketPage(Component):
|
class BuyTicketPage(Component):
|
||||||
@@ -19,24 +19,14 @@ class BuyTicketPage(Component):
|
|||||||
popup_message: str = ""
|
popup_message: str = ""
|
||||||
is_popup_success: bool = False
|
is_popup_success: bool = False
|
||||||
is_buying_enabled: bool = False
|
is_buying_enabled: bool = False
|
||||||
is_user_logged_in: bool = False
|
|
||||||
|
|
||||||
@event.on_populate
|
@event.on_populate
|
||||||
async def on_populate(self) -> None:
|
async def on_populate(self) -> None:
|
||||||
self.session[RefreshService].subscribe(self.on_populate)
|
|
||||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Ticket kaufen")
|
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Ticket kaufen")
|
||||||
try:
|
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||||
user_id = self.session[UserSession].user_id
|
|
||||||
except KeyError:
|
|
||||||
self.user = None
|
|
||||||
else:
|
|
||||||
self.user = await self.session[UserService].get_user(user_id)
|
|
||||||
if self.user is None: # No user logged in
|
if self.user is None: # No user logged in
|
||||||
self.is_buying_enabled = False
|
self.is_buying_enabled = False
|
||||||
self.is_user_logged_in = False
|
|
||||||
self.user_ticket = None
|
|
||||||
else: # User is logged in
|
else: # User is logged in
|
||||||
self.is_user_logged_in = True
|
|
||||||
possible_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id)
|
possible_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id)
|
||||||
self.user_ticket = possible_ticket
|
self.user_ticket = possible_ticket
|
||||||
if possible_ticket is not None: # User already has a ticket
|
if possible_ticket is not None: # User already has a ticket
|
||||||
@@ -77,29 +67,17 @@ class BuyTicketPage(Component):
|
|||||||
|
|
||||||
def build(self) -> Component:
|
def build(self) -> Component:
|
||||||
ticket_infos = self.session[ConfigurationService].get_ticket_info()
|
ticket_infos = self.session[ConfigurationService].get_ticket_info()
|
||||||
header = Column(
|
header = Text(
|
||||||
Text(
|
"Tickets & Preise",
|
||||||
"Tickets & Preise",
|
style=TextStyle(
|
||||||
style=TextStyle(
|
fill=self.session.theme.background_color,
|
||||||
fill=self.session.theme.background_color,
|
font_size=1.2
|
||||||
font_size=1.2
|
|
||||||
),
|
|
||||||
margin_top=2,
|
|
||||||
align_x=0.5
|
|
||||||
),
|
),
|
||||||
spacing=0.2
|
margin_top=2,
|
||||||
|
margin_bottom=2,
|
||||||
|
align_x=0.5
|
||||||
)
|
)
|
||||||
|
|
||||||
if not self.is_user_logged_in:
|
|
||||||
header.add(Text(
|
|
||||||
"Du musst eingeloggt sein\num ein Ticket zu kaufen",
|
|
||||||
style=TextStyle(
|
|
||||||
fill=self.session.theme.background_color,
|
|
||||||
font_size=0.6
|
|
||||||
),
|
|
||||||
align_x=0.5
|
|
||||||
))
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
MainViewContentBox(
|
MainViewContentBox(
|
||||||
Column(
|
Column(
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
from typing import Optional
|
from typing import Optional, Callable
|
||||||
|
|
||||||
from rio import Column, Component, event, TextStyle, Text, Spacer, Revealer, SwitcherBar, SwitcherBarChangeEvent, ProgressCircle
|
from rio import Column, Component, event, TextStyle, Text, Spacer, Revealer, SwitcherBar, SwitcherBarChangeEvent, ProgressCircle
|
||||||
|
|
||||||
from src.ezgg_lan_manager import ConfigurationService, CateringService
|
from src.ez_lan_manager import ConfigurationService, CateringService
|
||||||
from src.ezgg_lan_manager.components.CateringSelectionItem import CateringSelectionItem
|
from src.ez_lan_manager.components.CateringSelectionItem import CateringSelectionItem
|
||||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||||
from src.ezgg_lan_manager.components.ShoppingCartAndOrders import ShoppingCartAndOrders
|
from src.ez_lan_manager.components.ShoppingCartAndOrders import ShoppingCartAndOrders
|
||||||
from src.ezgg_lan_manager.services.RefreshService import RefreshService
|
from src.ez_lan_manager.types.CateringMenuItem import CateringMenuItemCategory, CateringMenuItem
|
||||||
from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItemCategory, CateringMenuItem
|
from src.ez_lan_manager.types.SessionStorage import SessionStorage
|
||||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
|
||||||
|
|
||||||
|
|
||||||
class CateringPage(Component):
|
class CateringPage(Component):
|
||||||
@@ -16,9 +15,11 @@ class CateringPage(Component):
|
|||||||
all_menu_items: Optional[list[CateringMenuItem]] = None
|
all_menu_items: Optional[list[CateringMenuItem]] = None
|
||||||
shopping_cart_and_orders: list[ShoppingCartAndOrders] = []
|
shopping_cart_and_orders: list[ShoppingCartAndOrders] = []
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self.session[SessionStorage].subscribe_to_logged_in_or_out_event(self.__class__.__name__, self.on_user_logged_in_status_changed)
|
||||||
|
|
||||||
@event.on_populate
|
@event.on_populate
|
||||||
async def on_populate(self) -> None:
|
async def on_populate(self) -> None:
|
||||||
self.session[RefreshService].subscribe(self.on_populate)
|
|
||||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Catering")
|
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Catering")
|
||||||
self.all_menu_items = await self.session[CateringService].get_menu()
|
self.all_menu_items = await self.session[CateringService].get_menu()
|
||||||
|
|
||||||
@@ -33,10 +34,7 @@ class CateringPage(Component):
|
|||||||
return list(filter(lambda item: item.category == category, all_menu_items))
|
return list(filter(lambda item: item.category == category, all_menu_items))
|
||||||
|
|
||||||
def build(self) -> Component:
|
def build(self) -> Component:
|
||||||
try:
|
user_id = self.session[SessionStorage].user_id
|
||||||
user_id = self.session[UserSession].user_id
|
|
||||||
except KeyError:
|
|
||||||
user_id = None
|
|
||||||
if len(self.shopping_cart_and_orders) == 0:
|
if len(self.shopping_cart_and_orders) == 0:
|
||||||
self.shopping_cart_and_orders.append(ShoppingCartAndOrders())
|
self.shopping_cart_and_orders.append(ShoppingCartAndOrders())
|
||||||
if len(self.shopping_cart_and_orders) > 1:
|
if len(self.shopping_cart_and_orders) > 1:
|
||||||
@@ -3,93 +3,94 @@ from typing import Optional
|
|||||||
|
|
||||||
from rio import Text, Column, TextStyle, Component, event, TextInput, MultiLineTextInput, Row, Button
|
from rio import Text, Column, TextStyle, Component, event, TextInput, MultiLineTextInput, Row, Button
|
||||||
|
|
||||||
from src.ezgg_lan_manager import ConfigurationService, UserService, MailingService
|
from src.ez_lan_manager import ConfigurationService, UserService, MailingService
|
||||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
from src.ez_lan_manager.components.AnimatedText import AnimatedText
|
||||||
from src.ezgg_lan_manager.types.User import User
|
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
from src.ez_lan_manager.types.SessionStorage import SessionStorage
|
||||||
|
from src.ez_lan_manager.types.User import User
|
||||||
|
|
||||||
|
|
||||||
class ContactPage(Component):
|
class ContactPage(Component):
|
||||||
# Workaround: Can not reassign this value without rio triggering refresh
|
# Workaround: Can not reassign this value without rio triggering refresh
|
||||||
# Using list to bypass this behavior
|
# Using list to bypass this behavior
|
||||||
last_message_sent: list[datetime] = [datetime(day=1, month=1, year=2000)]
|
last_message_sent: list[datetime] = [datetime(day=1, month=1, year=2000)]
|
||||||
|
display_printing: list[bool] = [False]
|
||||||
user: Optional[User] = None
|
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
|
@event.on_populate
|
||||||
async def on_populate(self) -> None:
|
async def on_populate(self) -> None:
|
||||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Kontakt")
|
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Kontakt")
|
||||||
try:
|
if self.session[SessionStorage].user_id is not None:
|
||||||
self.user = await self.session[UserService].get_user(self.session[UserSession].user_id)
|
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||||
except KeyError:
|
else:
|
||||||
self.user = None
|
self.user = None
|
||||||
self.e_mail = "" if not self.user else self.user.user_mail
|
|
||||||
|
|
||||||
async def on_send_pressed(self) -> None:
|
async def on_send_pressed(self) -> None:
|
||||||
error_msg = ""
|
error_msg = ""
|
||||||
self.submit_button_is_loading = True
|
self.submit_button.is_loading = True
|
||||||
|
self.submit_button.force_refresh()
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
if not self.e_mail:
|
if not self.email_input.text:
|
||||||
error_msg = "E-Mail darf nicht leer sein!"
|
error_msg = "E-Mail darf nicht leer sein!"
|
||||||
elif not self.subject:
|
elif not self.subject_input.text:
|
||||||
error_msg = "Betreff darf nicht leer sein!"
|
error_msg = "Betreff darf nicht leer sein!"
|
||||||
elif not self.message:
|
elif not self.message_input.text:
|
||||||
error_msg = "Nachricht darf nicht leer sein!"
|
error_msg = "Nachricht darf nicht leer sein!"
|
||||||
elif (now - self.last_message_sent[0]) < timedelta(minutes=1):
|
elif (now - self.last_message_sent[0]) < timedelta(minutes=1):
|
||||||
error_msg = "Immer mit der Ruhe!"
|
error_msg = "Immer mit der Ruhe!"
|
||||||
|
|
||||||
if error_msg:
|
if error_msg:
|
||||||
self.submit_button_is_loading = False
|
self.submit_button.is_loading = False
|
||||||
self.is_success = False
|
await self.animated_text.display_text(False, error_msg)
|
||||||
self.response_message = error_msg
|
|
||||||
return
|
return
|
||||||
|
|
||||||
mail_recipient = self.session[ConfigurationService].get_lan_info().organizer_mail
|
mail_recipient = self.session[ConfigurationService].get_lan_info().organizer_mail
|
||||||
msg = (f"Kontaktformular vom {now.strftime('%d.%m.%Y %H:%M')}:\n\n"
|
msg = (f"Kontaktformular vom {now.strftime('%d.%m.%Y %H:%M')}:\n\n"
|
||||||
f"Betreff: {self.subject}\n"
|
f"Betreff: {self.subject_input.text}\n"
|
||||||
f"Absender: {self.e_mail}\n\n"
|
f"Absender: {self.email_input.text}\n\n"
|
||||||
f"Inhalt:\n"
|
f"Inhalt:\n"
|
||||||
f"{self.message}\n")
|
f"{self.message_input.text}\n")
|
||||||
|
|
||||||
await self.session[MailingService].send_email("Kontaktformular-Mitteilung", msg, mail_recipient)
|
await self.session[MailingService].send_email("Kontaktformular-Mitteilung", msg, mail_recipient)
|
||||||
self.last_message_sent[0] = datetime.now()
|
self.last_message_sent[0] = datetime.now()
|
||||||
self.submit_button_is_loading = False
|
self.submit_button.is_loading = False
|
||||||
self.is_success = True
|
await self.animated_text.display_text(True, "Nachricht erfolgreich gesendet!")
|
||||||
self.response_message = "Nachricht erfolgreich gesendet!"
|
|
||||||
|
|
||||||
def build(self) -> Component:
|
def build(self) -> Component:
|
||||||
email_input = TextInput(
|
self.animated_text = AnimatedText(
|
||||||
|
margin_top=2,
|
||||||
|
margin_bottom=1,
|
||||||
|
align_x=0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
self.email_input = TextInput(
|
||||||
label="E-Mail Adresse",
|
label="E-Mail Adresse",
|
||||||
text=self.bind().e_mail,
|
text="" if not self.user else self.user.user_mail,
|
||||||
margin_left=1,
|
margin_left=1,
|
||||||
margin_right=1,
|
margin_right=1,
|
||||||
margin_bottom=1,
|
margin_bottom=1,
|
||||||
grow_x=True
|
grow_x=True
|
||||||
)
|
)
|
||||||
|
|
||||||
subject_input = TextInput(
|
self.subject_input = TextInput(
|
||||||
label="Betreff",
|
label="Betreff",
|
||||||
text=self.bind().subject,
|
text="",
|
||||||
margin_left=1,
|
margin_left=1,
|
||||||
margin_right=1,
|
margin_right=1,
|
||||||
margin_bottom=1,
|
margin_bottom=1,
|
||||||
grow_x=True
|
grow_x=True
|
||||||
)
|
)
|
||||||
|
|
||||||
message_input = MultiLineTextInput(
|
self.message_input = MultiLineTextInput(
|
||||||
label="Deine Nachricht an uns",
|
label="Deine Nachricht an uns",
|
||||||
text=self.bind().message,
|
text="",
|
||||||
margin_left=1,
|
margin_left=1,
|
||||||
margin_right=1,
|
margin_right=1,
|
||||||
margin_bottom=1,
|
margin_bottom=1,
|
||||||
min_height=5
|
min_height=5
|
||||||
)
|
)
|
||||||
|
|
||||||
submit_button = Button(
|
self.submit_button = Button(
|
||||||
content=Text(
|
content=Text(
|
||||||
"Absenden",
|
"Absenden",
|
||||||
style=TextStyle(fill=self.session.theme.success_color, font_size=0.9),
|
style=TextStyle(fill=self.session.theme.success_color, font_size=0.9),
|
||||||
@@ -101,8 +102,7 @@ class ContactPage(Component):
|
|||||||
shape="rectangle",
|
shape="rectangle",
|
||||||
style="major",
|
style="major",
|
||||||
color="primary",
|
color="primary",
|
||||||
on_press=self.on_send_pressed,
|
on_press=self.on_send_pressed
|
||||||
is_loading=self.bind().submit_button_is_loading
|
|
||||||
)
|
)
|
||||||
return Column(
|
return Column(
|
||||||
MainViewContentBox(
|
MainViewContentBox(
|
||||||
@@ -117,21 +117,12 @@ class ContactPage(Component):
|
|||||||
margin_bottom=1,
|
margin_bottom=1,
|
||||||
align_x=0.5
|
align_x=0.5
|
||||||
),
|
),
|
||||||
email_input,
|
self.email_input,
|
||||||
subject_input,
|
self.subject_input,
|
||||||
message_input,
|
self.message_input,
|
||||||
Row(
|
Row(
|
||||||
Text(
|
self.animated_text,
|
||||||
text=self.bind().response_message,
|
self.submit_button,
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@@ -5,9 +5,9 @@ from typing import * # type: ignore
|
|||||||
|
|
||||||
from rio import Component, event, Spacer, Card, Container, Column, Row, TextStyle, Color, Text
|
from rio import Component, event, Spacer, Card, Container, Column, Row, TextStyle, Color, Text
|
||||||
|
|
||||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
from src.ez_lan_manager.services.DatabaseService import DatabaseService
|
||||||
from src.ezgg_lan_manager import ConfigurationService
|
from src.ez_lan_manager import ConfigurationService
|
||||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||||
|
|
||||||
|
|
||||||
class DbErrorPage(Component):
|
class DbErrorPage(Component):
|
||||||
@@ -62,7 +62,7 @@ class DbErrorPage(Component):
|
|||||||
Row(
|
Row(
|
||||||
Spacer(grow_x=True, grow_y=False),
|
Spacer(grow_x=True, grow_y=False),
|
||||||
Card(
|
Card(
|
||||||
content=Text(f"EZGG LAN Manager Version {self.session[ConfigurationService].APP_VERSION} © EZ GG e.V.", align_x=0.5, align_y=0.5, style=TextStyle(fill=self.session.theme.primary_color, font_size=0.5)),
|
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,
|
color=self.session.theme.neutral_color,
|
||||||
corner_radius=(0, 0, 0.5, 0.5),
|
corner_radius=(0, 0, 0.5, 0.5),
|
||||||
grow_x=False,
|
grow_x=False,
|
||||||
@@ -82,7 +82,7 @@ class DbErrorPage(Component):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return Text(
|
return Text(
|
||||||
"Der EZGG LAN Manager wird\nauf mobilen Endgeräten nur\nim Querformat unterstützt.\nBitte drehe dein Gerät.",
|
"Der EZ LAN Manager wird\nauf mobilen Endgeräten nur\nim Querformat unterstützt.\nBitte drehe dein Gerät.",
|
||||||
align_x=0.5,
|
align_x=0.5,
|
||||||
align_y=0.5,
|
align_y=0.5,
|
||||||
style=TextStyle(fill=Color.from_hex("FFFFFF"), font_size=0.8)
|
style=TextStyle(fill=Color.from_hex("FFFFFF"), font_size=0.8)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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,7 +1,7 @@
|
|||||||
from rio import Column, Component, event, TextStyle, Text, Revealer
|
from rio import Column, Component, event, TextStyle, Text, Revealer
|
||||||
|
|
||||||
from src.ezgg_lan_manager import ConfigurationService
|
from src.ez_lan_manager import ConfigurationService
|
||||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||||
|
|
||||||
FAQ: list[list[str]] = [
|
FAQ: list[list[str]] = [
|
||||||
["Wie melde ich mich für die LAN an?",
|
["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 email_validator import validate_email, EmailNotValidError
|
||||||
from rio import Column, Component, event, Text, TextStyle, TextInput, TextInputChangeEvent, Button
|
from rio import Column, Component, event, Text, TextStyle, TextInput, TextInputChangeEvent, Button
|
||||||
|
|
||||||
from src.ezgg_lan_manager import ConfigurationService, UserService, MailingService
|
from src.ez_lan_manager import ConfigurationService, UserService, MailingService
|
||||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||||
|
|
||||||
|
|
||||||
class ForgotPasswordPage(Component):
|
class ForgotPasswordPage(Component):
|
||||||
@@ -27,11 +27,11 @@ class ForgotPasswordPage(Component):
|
|||||||
user = await user_service.get_user(self.email_input.text.strip())
|
user = await user_service.get_user(self.email_input.text.strip())
|
||||||
if user is not None:
|
if user is not None:
|
||||||
new_password = "".join(choices(user_service.ALLOWED_USER_NAME_SYMBOLS, k=16))
|
new_password = "".join(choices(user_service.ALLOWED_USER_NAME_SYMBOLS, k=16))
|
||||||
user.user_fallback_password = sha256(new_password.encode(encoding="utf-8")).hexdigest()
|
user.user_password = sha256(new_password.encode(encoding="utf-8")).hexdigest()
|
||||||
await user_service.update_user(user)
|
await user_service.update_user(user)
|
||||||
await mailing_service.send_email(
|
await mailing_service.send_email(
|
||||||
subject=f"Dein neues Passwort für {lan_info.name}",
|
subject=f"Dein neues Passwort für {lan_info.name}",
|
||||||
body=f"Du hast für den EZGG-LAN Manager der {lan_info.name} ein neues Passwort angefragt. "
|
body=f"Du hast für den EZ-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"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",
|
f"ignoriere diese E-Mail.\n\nLiebe Grüße\nDein {lan_info.name} - Team",
|
||||||
receiver=self.email_input.text.strip()
|
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 rio import Column, Component, event, TextStyle, Text, Button, Row, TextInput, Spacer, TextInputChangeEvent
|
||||||
|
|
||||||
from src.ezgg_lan_manager import ConfigurationService, UserService, TicketingService, SeatingService
|
from src.ez_lan_manager import ConfigurationService, UserService, TicketingService, SeatingService
|
||||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||||
from src.ezgg_lan_manager.types.Seat import Seat
|
from src.ez_lan_manager.types.Seat import Seat
|
||||||
from src.ezgg_lan_manager.types.User import User
|
from src.ez_lan_manager.types.User import User
|
||||||
|
|
||||||
|
|
||||||
class GuestsPage(Component):
|
class GuestsPage(Component):
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from rio import Text, Column, TextStyle, Component, event, Link, Color
|
from rio import Text, Column, TextStyle, Component, event, Link, Color
|
||||||
|
|
||||||
from src.ezgg_lan_manager import ConfigurationService
|
from src.ez_lan_manager import ConfigurationService
|
||||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||||
|
|
||||||
|
|
||||||
class ImprintPage(Component):
|
class ImprintPage(Component):
|
||||||
@@ -5,11 +5,11 @@ from typing import Optional, Callable
|
|||||||
|
|
||||||
from rio import Column, Component, event, TextStyle, Text, Spacer, PointerEvent, Button, Popup, Card, Row
|
from rio import Column, Component, event, TextStyle, Text, Spacer, PointerEvent, Button, Popup, Card, Row
|
||||||
|
|
||||||
from src.ezgg_lan_manager import ConfigurationService, CateringService, SeatingService, AccountingService
|
from src.ez_lan_manager import ConfigurationService, CateringService, SeatingService, AccountingService
|
||||||
from src.ezgg_lan_manager.components.CateringManagementOrderDisplay import CateringManagementOrderDisplay
|
from src.ez_lan_manager.components.CateringManagementOrderDisplay import CateringManagementOrderDisplay
|
||||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus
|
from src.ez_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus
|
||||||
from src.ezgg_lan_manager.types.Seat import Seat
|
from src.ez_lan_manager.types.Seat import Seat
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
|
|
||||||
@@ -5,11 +5,11 @@ from time import strptime
|
|||||||
|
|
||||||
from rio import Column, Component, event, TextStyle, Text
|
from rio import Column, Component, event, TextStyle, Text
|
||||||
|
|
||||||
from src.ezgg_lan_manager import ConfigurationService, UserService
|
from src.ez_lan_manager import ConfigurationService, UserService
|
||||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||||
from src.ezgg_lan_manager.components.NewsPost import EditableNewsPost
|
from src.ez_lan_manager.components.NewsPost import EditableNewsPost
|
||||||
from src.ezgg_lan_manager.services.NewsService import NewsService
|
from src.ez_lan_manager.services.NewsService import NewsService
|
||||||
from src.ezgg_lan_manager.types.News import News
|
from src.ez_lan_manager.types.News import News
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
|
|
||||||
@@ -78,8 +78,8 @@ class ManageNewsPage(Component):
|
|||||||
Column(
|
Column(
|
||||||
Text(
|
Text(
|
||||||
text="News Verwaltung",
|
text="News Verwaltung",
|
||||||
fill=self.session.theme.background_color,
|
|
||||||
style=TextStyle(
|
style=TextStyle(
|
||||||
|
fill=self.session.theme.background_color,
|
||||||
font_size=1.2
|
font_size=1.2
|
||||||
),
|
),
|
||||||
margin_top=2,
|
margin_top=2,
|
||||||
@@ -88,8 +88,8 @@ class ManageNewsPage(Component):
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
text="Neuen News Post erstellen",
|
text="Neuen News Post erstellen",
|
||||||
fill=self.session.theme.background_color,
|
|
||||||
style=TextStyle(
|
style=TextStyle(
|
||||||
|
fill=self.session.theme.background_color,
|
||||||
font_size=1.1
|
font_size=1.1
|
||||||
),
|
),
|
||||||
margin_top=2,
|
margin_top=2,
|
||||||
@@ -106,8 +106,8 @@ class ManageNewsPage(Component):
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
text="Post erfolgreich erstellt",
|
text="Post erfolgreich erstellt",
|
||||||
fill=self.session.theme.success_color,
|
|
||||||
style=TextStyle(
|
style=TextStyle(
|
||||||
|
fill=self.session.theme.success_color,
|
||||||
font_size=0.7 if self.show_success_message else 0
|
font_size=0.7 if self.show_success_message else 0
|
||||||
),
|
),
|
||||||
margin_top=0.1,
|
margin_top=0.1,
|
||||||
@@ -116,8 +116,8 @@ class ManageNewsPage(Component):
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
text="Bisherige Posts",
|
text="Bisherige Posts",
|
||||||
fill=self.session.theme.background_color,
|
|
||||||
style=TextStyle(
|
style=TextStyle(
|
||||||
|
fill=self.session.theme.background_color,
|
||||||
font_size=1.1
|
font_size=1.1
|
||||||
),
|
),
|
||||||
margin_top=2,
|
margin_top=2,
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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()
|
||||||
|
)
|
||||||
@@ -3,17 +3,17 @@ from dataclasses import field
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from rio import Column, Component, event, TextStyle, Text, TextInput, ThemeContextSwitcher, Grid, \
|
from rio import Column, Component, event, TextStyle, Text, TextInput, ThemeContextSwitcher, Grid, \
|
||||||
PointerEventListener, PointerEvent, Rectangle, Color, TextInputChangeEvent, Spacer, Row, Switch, \
|
PointerEventListener, PointerEvent, Rectangle, CursorStyle, Color, TextInputChangeEvent, Spacer, Row, Switch, \
|
||||||
SwitchChangeEvent, EventHandler
|
SwitchChangeEvent, EventHandler
|
||||||
|
|
||||||
from src.ezgg_lan_manager import ConfigurationService, UserService, AccountingService, SeatingService, MailingService
|
from src.ez_lan_manager import ConfigurationService, UserService, AccountingService, SeatingService
|
||||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||||
from src.ezgg_lan_manager.components.NewTransactionForm import NewTransactionForm
|
from src.ez_lan_manager.components.NewTransactionForm import NewTransactionForm
|
||||||
from src.ezgg_lan_manager.components.UserEditForm import UserEditForm
|
from src.ez_lan_manager.components.UserEditForm import UserEditForm
|
||||||
from src.ezgg_lan_manager.services.AccountingService import InsufficientFundsError
|
from src.ez_lan_manager.services.AccountingService import InsufficientFundsError
|
||||||
from src.ezgg_lan_manager.types.Transaction import Transaction
|
from src.ez_lan_manager.types.SessionStorage import SessionStorage
|
||||||
from src.ezgg_lan_manager.types.User import User
|
from src.ez_lan_manager.types.Transaction import Transaction
|
||||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
from src.ez_lan_manager.types.User import User
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ class ClickableGridContent(Component):
|
|||||||
grow_x=True
|
grow_x=True
|
||||||
),
|
),
|
||||||
fill=Color.TRANSPARENT,
|
fill=Color.TRANSPARENT,
|
||||||
cursor="pointer"
|
cursor=CursorStyle.POINTER
|
||||||
),
|
),
|
||||||
on_pointer_enter=self.on_mouse_enter,
|
on_pointer_enter=self.on_mouse_enter,
|
||||||
on_pointer_leave=self.on_mouse_leave,
|
on_pointer_leave=self.on_mouse_leave,
|
||||||
@@ -84,11 +84,7 @@ class ManageUsersPage(Component):
|
|||||||
await self.session[UserService].update_user(self.selected_user)
|
await self.session[UserService].update_user(self.selected_user)
|
||||||
|
|
||||||
async def on_new_transaction(self, transaction: Transaction) -> None:
|
async def on_new_transaction(self, transaction: Transaction) -> None:
|
||||||
try:
|
if not self.session[SessionStorage].is_team_member: # Better safe than sorry
|
||||||
user = await self.session[UserService].get_user(self.session[UserSession].user_id)
|
|
||||||
if not user.is_team_member: # Better safe than sorry
|
|
||||||
return
|
|
||||||
except KeyError:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info(f"Got new transaction for user with ID '{transaction.user_id}' over "
|
logger.info(f"Got new transaction for user with ID '{transaction.user_id}' over "
|
||||||
@@ -108,17 +104,11 @@ class ManageUsersPage(Component):
|
|||||||
self.accounting_section_result_success = False
|
self.accounting_section_result_success = False
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
new_total_balance = await self.session[AccountingService].add_balance(
|
await self.session[AccountingService].add_balance(
|
||||||
transaction.user_id,
|
transaction.user_id,
|
||||||
transaction.value,
|
transaction.value,
|
||||||
transaction.reference
|
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_text = f"Guthaben {'entfernt' if transaction.is_debit else 'hinzugefügt'}!"
|
||||||
self.accounting_section_result_success = True
|
self.accounting_section_result_success = True
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
from rio import Column, Component, event
|
from rio import Column, Component, event
|
||||||
|
|
||||||
from src.ezgg_lan_manager import ConfigurationService, NewsService
|
from src.ez_lan_manager import ConfigurationService, NewsService
|
||||||
from src.ezgg_lan_manager.components.NewsPost import NewsPost
|
from src.ez_lan_manager.components.NewsPost import NewsPost
|
||||||
from src.ezgg_lan_manager.types.News import News
|
from src.ez_lan_manager.types.News import News
|
||||||
|
|
||||||
|
|
||||||
class NewsPage(Component):
|
class NewsPage(Component):
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from rio import Column, Component, event
|
from rio import Column, Component, event
|
||||||
|
|
||||||
from src.ezgg_lan_manager import ConfigurationService
|
from src.ez_lan_manager import ConfigurationService
|
||||||
from src.ezgg_lan_manager.components.NewsPost import NewsPost
|
from src.ez_lan_manager.components.NewsPost import NewsPost
|
||||||
|
|
||||||
|
|
||||||
class PlaceholderPage(Component):
|
class PlaceholderPage(Component):
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
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,7 +1,7 @@
|
|||||||
from rio import Column, Component, event, TextStyle, Text, Revealer
|
from rio import Column, Component, event, TextStyle, Text, Revealer
|
||||||
|
|
||||||
from src.ezgg_lan_manager import ConfigurationService
|
from src.ez_lan_manager import ConfigurationService
|
||||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||||
|
|
||||||
RULES: list[str] = [
|
RULES: list[str] = [
|
||||||
"Respektvolles Verhalten: Sei höflich und respektvoll gegenüber anderen Gästen und dem Team.",
|
"Respektvolles Verhalten: Sei höflich und respektvoll gegenüber anderen Gästen und dem Team.",
|
||||||
@@ -5,15 +5,15 @@ from typing import Optional
|
|||||||
|
|
||||||
from rio import Text, Column, TextStyle, Component, event, PressEvent, ProgressCircle
|
from rio import Text, Column, TextStyle, Component, event, PressEvent, ProgressCircle
|
||||||
|
|
||||||
from src.ezgg_lan_manager import ConfigurationService, SeatingService, TicketingService, UserService
|
from src.ez_lan_manager import ConfigurationService, SeatingService, TicketingService, UserService
|
||||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||||
from src.ezgg_lan_manager.components.SeatingPlan import SeatingPlan, SeatingPlanLegend
|
from src.ez_lan_manager.components.SeatingPlan import SeatingPlan, SeatingPlanLegend
|
||||||
from src.ezgg_lan_manager.components.SeatingPlanInfoBox import SeatingPlanInfoBox
|
from src.ez_lan_manager.components.SeatingPlanInfoBox import SeatingPlanInfoBox
|
||||||
from src.ezgg_lan_manager.components.SeatingPurchaseBox import SeatingPurchaseBox
|
from src.ez_lan_manager.components.SeatingPurchaseBox import SeatingPurchaseBox
|
||||||
from src.ezgg_lan_manager.services.SeatingService import NoTicketError, SeatNotFoundError, WrongCategoryError, SeatAlreadyTakenError
|
from src.ez_lan_manager.services.SeatingService import NoTicketError, SeatNotFoundError, WrongCategoryError, SeatAlreadyTakenError
|
||||||
from src.ezgg_lan_manager.types.Seat import Seat
|
from src.ez_lan_manager.types.Seat import Seat
|
||||||
from src.ezgg_lan_manager.types.User import User
|
from src.ez_lan_manager.types.SessionStorage import SessionStorage
|
||||||
from src.ezgg_lan_manager.types.UserSession import UserSession
|
from src.ez_lan_manager.types.User import User
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
|
|
||||||
@@ -30,17 +30,13 @@ class SeatingPlanPage(Component):
|
|||||||
purchase_box_loading: bool = False
|
purchase_box_loading: bool = False
|
||||||
purchase_box_success_msg: Optional[str] = None
|
purchase_box_success_msg: Optional[str] = None
|
||||||
purchase_box_error_msg: Optional[str] = None
|
purchase_box_error_msg: Optional[str] = None
|
||||||
seating_info_text = ""
|
|
||||||
is_booking_blocked: bool = False
|
is_booking_blocked: bool = False
|
||||||
|
|
||||||
@event.on_populate
|
@event.on_populate
|
||||||
async def on_populate(self) -> None:
|
async def on_populate(self) -> None:
|
||||||
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Sitzplan")
|
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Sitzplan")
|
||||||
self.seating_info = await self.session[SeatingService].get_seating()
|
self.seating_info = await self.session[SeatingService].get_seating()
|
||||||
try:
|
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||||
self.user = await self.session[UserService].get_user(self.session[UserSession].user_id)
|
|
||||||
except KeyError:
|
|
||||||
self.user = None
|
|
||||||
if not self.user:
|
if not self.user:
|
||||||
self.is_booking_blocked = True
|
self.is_booking_blocked = True
|
||||||
else:
|
else:
|
||||||
@@ -51,7 +47,6 @@ class SeatingPlanPage(Component):
|
|||||||
self.is_booking_blocked = True
|
self.is_booking_blocked = True
|
||||||
|
|
||||||
async def on_seat_clicked(self, seat_id: str, _: PressEvent) -> None:
|
async def on_seat_clicked(self, seat_id: str, _: PressEvent) -> None:
|
||||||
self.seating_info_text = ""
|
|
||||||
self.show_info_box = True
|
self.show_info_box = True
|
||||||
self.show_purchase_box = False
|
self.show_purchase_box = False
|
||||||
seat = next(filter(lambda s: s.seat_id == seat_id, self.seating_info), None)
|
seat = next(filter(lambda s: s.seat_id == seat_id, self.seating_info), None)
|
||||||
@@ -67,12 +62,6 @@ class SeatingPlanPage(Component):
|
|||||||
else:
|
else:
|
||||||
self.current_seat_occupant = None
|
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:
|
def set_error(self, msg: str) -> None:
|
||||||
self.purchase_box_error_msg = msg
|
self.purchase_box_error_msg = msg
|
||||||
self.purchase_box_success_msg = None
|
self.purchase_box_success_msg = None
|
||||||
@@ -130,7 +119,7 @@ class SeatingPlanPage(Component):
|
|||||||
Column(
|
Column(
|
||||||
SeatingPlanInfoBox(seat_id=self.current_seat_id, seat_occupant=self.current_seat_occupant, seat_price=self.current_seat_price,
|
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,
|
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, override_text=self.seating_info_text),
|
purchase_cb=self.on_purchase_clicked),
|
||||||
SeatingPurchaseBox(
|
SeatingPurchaseBox(
|
||||||
show=self.show_purchase_box,
|
show=self.show_purchase_box,
|
||||||
seat_id=self.current_seat_id,
|
seat_id=self.current_seat_id,
|
||||||
@@ -143,7 +132,7 @@ class SeatingPlanPage(Component):
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
MainViewContentBox(
|
MainViewContentBox(
|
||||||
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
|
SeatingPlan(seat_clicked_cb=self.on_seat_clicked, seating_info=self.seating_info) if self.seating_info else
|
||||||
Column(ProgressCircle(color=self.session.theme.secondary_color, margin=3),
|
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))
|
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 rio import Column, Component, event, TextStyle, Text
|
||||||
|
|
||||||
from src.ezgg_lan_manager import ConfigurationService
|
from src.ez_lan_manager import ConfigurationService
|
||||||
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
|
from src.ez_lan_manager.components.MainViewContentBox import MainViewContentBox
|
||||||
|
|
||||||
|
|
||||||
class PAGENAME(Component):
|
class PAGENAME(Component):
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
@@ -19,9 +19,3 @@ from .ManageNewsPage import ManageNewsPage
|
|||||||
from .ManageUsersPage import ManageUsersPage
|
from .ManageUsersPage import ManageUsersPage
|
||||||
from .ManageCateringPage import ManageCateringPage
|
from .ManageCateringPage import ManageCateringPage
|
||||||
from .ManageTournamentsPage import ManageTournamentsPage
|
from .ManageTournamentsPage import ManageTournamentsPage
|
||||||
from .OverviewPage import OverviewPage
|
|
||||||
from .TournamentDetailsPage import TournamentDetailsPage
|
|
||||||
from .TournamentRulesPage import TournamentRulesPage
|
|
||||||
from .ConwayPage import ConwayPage
|
|
||||||
from .TeamsPage import TeamsPage
|
|
||||||
from .AdminNavigationPage import AdminNavigationPage
|
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
import io
|
|
||||||
import logging
|
import logging
|
||||||
import qrcode
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal, ROUND_DOWN
|
from decimal import Decimal, ROUND_DOWN
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
from src.ez_lan_manager.services.DatabaseService import DatabaseService
|
||||||
from src.ezgg_lan_manager.types.Transaction import Transaction
|
from src.ez_lan_manager.types.Transaction import Transaction
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
|
|
||||||
@@ -68,37 +65,9 @@ class AccountingService:
|
|||||||
return await self._db_service.get_all_transactions_for_user(user_id)
|
return await self._db_service.get_all_transactions_for_user(user_id)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def make_euro_string_from_decimal(euros: Optional[Decimal]) -> str:
|
def make_euro_string_from_decimal(euros: Decimal) -> str:
|
||||||
"""
|
"""
|
||||||
Internally, all money values are euros as decimal. Only when showing them to the user we generate a string.
|
Internally, all money values are euros as decimal. Only when showing them to the user we generate a string.
|
||||||
"""
|
"""
|
||||||
if euros is None:
|
|
||||||
return "0.00 €"
|
|
||||||
rounded_decimal = str(euros.quantize(Decimal(".01"), rounding=ROUND_DOWN))
|
rounded_decimal = str(euros.quantize(Decimal(".01"), rounding=ROUND_DOWN))
|
||||||
return f"{rounded_decimal} €"
|
return f"{rounded_decimal} €"
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def make_payment_qr_image(beneficiary_name, beneficiary_bic, beneficiary_iban, text, amount_euros=None) -> bytes:
|
|
||||||
text = text.replace("\n",";")
|
|
||||||
amount_formatted = "EUR{:.2f}".format(amount_euros) if amount_euros else ""
|
|
||||||
epc_text = f"""BCD
|
|
||||||
002
|
|
||||||
1
|
|
||||||
SCT
|
|
||||||
{beneficiary_bic}
|
|
||||||
{beneficiary_name}
|
|
||||||
{beneficiary_iban}
|
|
||||||
{amount_formatted}
|
|
||||||
|
|
||||||
|
|
||||||
{text}
|
|
||||||
"""
|
|
||||||
qr = qrcode.QRCode(
|
|
||||||
version=6,
|
|
||||||
error_correction=qrcode.constants.ERROR_CORRECT_M,
|
|
||||||
)
|
|
||||||
qr.add_data(epc_text)
|
|
||||||
img = qr.make_image()
|
|
||||||
img_bytes = io.BytesIO()
|
|
||||||
img.save(img_bytes)
|
|
||||||
return img_bytes.getvalue()
|
|
||||||
@@ -3,12 +3,11 @@ from decimal import Decimal
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from src.ezgg_lan_manager.services.AccountingService import AccountingService
|
from src.ez_lan_manager.services.AccountingService import AccountingService
|
||||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
from src.ez_lan_manager.services.DatabaseService import DatabaseService
|
||||||
from src.ezgg_lan_manager.services.UserService import UserService
|
from src.ez_lan_manager.services.UserService import UserService
|
||||||
from src.ezgg_lan_manager.services.ReceiptPrintingService import ReceiptPrintingService
|
from src.ez_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus, CateringMenuItemsWithAmount
|
||||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus, CateringMenuItemsWithAmount
|
from src.ez_lan_manager.types.CateringMenuItem import CateringMenuItem, CateringMenuItemCategory
|
||||||
from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItem, CateringMenuItemCategory
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
|
|
||||||
@@ -26,11 +25,10 @@ class CateringError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class CateringService:
|
class CateringService:
|
||||||
def __init__(self, db_service: DatabaseService, accounting_service: AccountingService, user_service: UserService, receipt_printing_service: ReceiptPrintingService) -> None:
|
def __init__(self, db_service: DatabaseService, accounting_service: AccountingService, user_service: UserService):
|
||||||
self._db_service = db_service
|
self._db_service = db_service
|
||||||
self._accounting_service = accounting_service
|
self._accounting_service = accounting_service
|
||||||
self._user_service = user_service
|
self._user_service = user_service
|
||||||
self._receipt_printing_service = receipt_printing_service
|
|
||||||
self.cached_cart: dict[int, list[CateringMenuItem]] = {}
|
self.cached_cart: dict[int, list[CateringMenuItem]] = {}
|
||||||
|
|
||||||
# ORDERS
|
# ORDERS
|
||||||
@@ -54,7 +52,6 @@ class CateringService:
|
|||||||
await self._accounting_service.remove_balance(user_id, total_price, f"CATERING - {order.order_id}")
|
await self._accounting_service.remove_balance(user_id, total_price, f"CATERING - {order.order_id}")
|
||||||
logger.info(
|
logger.info(
|
||||||
f"User '{order.customer.user_name}' (ID:{order.customer.user_id}) ordered from catering for {self._accounting_service.make_euro_string_from_decimal(total_price)}")
|
f"User '{order.customer.user_name}' (ID:{order.customer.user_id}) ordered from catering for {self._accounting_service.make_euro_string_from_decimal(total_price)}")
|
||||||
await self._receipt_printing_service.print_order(user, order)
|
|
||||||
# await self.cancel_order(order) # ToDo: Check if commented out before commit. Un-comment to auto-cancel every placed order
|
# await self.cancel_order(order) # ToDo: Check if commented out before commit. Un-comment to auto-cancel every placed order
|
||||||
return order
|
return order
|
||||||
|
|
||||||
@@ -7,8 +7,8 @@ import tomllib
|
|||||||
|
|
||||||
from from_root import from_root
|
from from_root import from_root
|
||||||
|
|
||||||
from src.ezgg_lan_manager.types.ConfigurationTypes import DatabaseConfiguration, MailingServiceConfiguration, LanInfo, \
|
from src.ez_lan_manager.types.ConfigurationTypes import DatabaseConfiguration, MailingServiceConfiguration, LanInfo, \
|
||||||
SeatingConfiguration, TicketInfo, ReceiptPrintingConfiguration
|
SeatingConfiguration, TicketInfo
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
|
|
||||||
@@ -71,6 +71,15 @@ class ConfigurationService:
|
|||||||
logger.fatal("Error loading LAN Info, exiting...")
|
logger.fatal("Error loading LAN Info, exiting...")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
def get_seating_configuration(self) -> SeatingConfiguration:
|
||||||
|
try:
|
||||||
|
return SeatingConfiguration(
|
||||||
|
seats=self._config["seating"]
|
||||||
|
)
|
||||||
|
except KeyError:
|
||||||
|
logger.fatal("Error loading seating configuration, exiting...")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
def get_ticket_info(self) -> tuple[TicketInfo, ...]:
|
def get_ticket_info(self) -> tuple[TicketInfo, ...]:
|
||||||
try:
|
try:
|
||||||
return tuple([TicketInfo(
|
return tuple([TicketInfo(
|
||||||
@@ -86,19 +95,6 @@ class ConfigurationService:
|
|||||||
logger.fatal("Error loading seating configuration, exiting...")
|
logger.fatal("Error loading seating configuration, exiting...")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
def get_receipt_printing_configuration(self) -> ReceiptPrintingConfiguration:
|
|
||||||
try:
|
|
||||||
receipt_printing_configuration = self._config["receipt_printing"]
|
|
||||||
return ReceiptPrintingConfiguration(
|
|
||||||
host=receipt_printing_configuration["host"],
|
|
||||||
port=receipt_printing_configuration["port"],
|
|
||||||
order_print_endpoint=receipt_printing_configuration["order_print_endpoint"],
|
|
||||||
password=receipt_printing_configuration["password"]
|
|
||||||
)
|
|
||||||
except KeyError:
|
|
||||||
logger.fatal("Error loading Receipt Printing Configuration, exiting...")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def APP_VERSION(self) -> str:
|
def APP_VERSION(self) -> str:
|
||||||
return self._version
|
return self._version
|
||||||
@@ -6,19 +6,15 @@ from decimal import Decimal
|
|||||||
|
|
||||||
import aiomysql
|
import aiomysql
|
||||||
|
|
||||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder
|
from src.ez_lan_manager.types.CateringOrder import CateringOrder
|
||||||
from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItem, CateringMenuItemCategory
|
from src.ez_lan_manager.types.CateringMenuItem import CateringMenuItem, CateringMenuItemCategory
|
||||||
from src.ezgg_lan_manager.types.CateringOrder import CateringMenuItemsWithAmount, CateringOrderStatus
|
from src.ez_lan_manager.types.CateringOrder import CateringMenuItemsWithAmount, CateringOrderStatus
|
||||||
from src.ezgg_lan_manager.types.ConfigurationTypes import DatabaseConfiguration
|
from src.ez_lan_manager.types.ConfigurationTypes import DatabaseConfiguration
|
||||||
from src.ezgg_lan_manager.types.News import News
|
from src.ez_lan_manager.types.News import News
|
||||||
from src.ezgg_lan_manager.types.Participant import Participant
|
from src.ez_lan_manager.types.Seat import Seat
|
||||||
from src.ezgg_lan_manager.types.Seat import Seat
|
from src.ez_lan_manager.types.Ticket import Ticket
|
||||||
from src.ezgg_lan_manager.types.Team import TeamStatus, Team
|
from src.ez_lan_manager.types.Transaction import Transaction
|
||||||
from src.ezgg_lan_manager.types.Ticket import Ticket
|
from src.ez_lan_manager.types.User import User
|
||||||
from src.ezgg_lan_manager.types.Tournament import Tournament
|
|
||||||
from src.ezgg_lan_manager.types.TournamentBase import GameTitle, TournamentFormat, TournamentStatus, ParticipantType
|
|
||||||
from src.ezgg_lan_manager.types.Transaction import Transaction
|
|
||||||
from src.ezgg_lan_manager.types.User import User
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
|
|
||||||
@@ -62,8 +58,7 @@ class DatabaseService:
|
|||||||
password=self._database_config.db_password,
|
password=self._database_config.db_password,
|
||||||
db=self._database_config.db_name,
|
db=self._database_config.db_name,
|
||||||
minsize=1,
|
minsize=1,
|
||||||
maxsize=40,
|
maxsize=40
|
||||||
autocommit=True
|
|
||||||
)
|
)
|
||||||
except aiomysql.OperationalError:
|
except aiomysql.OperationalError:
|
||||||
return False
|
return False
|
||||||
@@ -76,65 +71,16 @@ class DatabaseService:
|
|||||||
user_name=data[1],
|
user_name=data[1],
|
||||||
user_mail=data[2],
|
user_mail=data[2],
|
||||||
user_password=data[3],
|
user_password=data[3],
|
||||||
user_fallback_password=data[4],
|
user_first_name=data[4],
|
||||||
user_first_name=data[5],
|
user_last_name=data[5],
|
||||||
user_last_name=data[6],
|
user_birth_day=data[6],
|
||||||
user_birth_day=data[7],
|
is_active=bool(data[7]),
|
||||||
is_active=bool(data[8]),
|
is_team_member=bool(data[8]),
|
||||||
is_team_member=bool(data[9]),
|
is_admin=bool(data[9]),
|
||||||
is_admin=bool(data[10]),
|
created_at=data[10],
|
||||||
created_at=data[11],
|
last_updated_at=data[11]
|
||||||
last_updated_at=data[12]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _parse_tournament_format(format_as_string: str) -> TournamentFormat:
|
|
||||||
if format_as_string == "SE_BO_1":
|
|
||||||
return TournamentFormat.SINGLE_ELIMINATION_BO_1
|
|
||||||
elif format_as_string == "SE_BO_3":
|
|
||||||
return TournamentFormat.SINGLE_ELIMINATION_BO_3
|
|
||||||
elif format_as_string == "SE_BO_5":
|
|
||||||
return TournamentFormat.SINGLE_ELIMINATION_BO_5
|
|
||||||
elif format_as_string == "DE_BO_1":
|
|
||||||
return TournamentFormat.DOUBLE_ELIMINATION_BO_1
|
|
||||||
elif format_as_string == "DE_BO_3":
|
|
||||||
return TournamentFormat.DOUBLE_ELIMINATION_BO_3
|
|
||||||
elif format_as_string == "DE_BO_5":
|
|
||||||
return TournamentFormat.DOUBLE_ELIMINATION_BO_5
|
|
||||||
elif format_as_string == "FFA":
|
|
||||||
return TournamentFormat.FFA
|
|
||||||
else:
|
|
||||||
# If this happens, database is FUBAR
|
|
||||||
raise RuntimeError(f"Unknown TournamentFormat: {format_as_string}")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _parse_tournament_status(status_as_string: str) -> TournamentStatus:
|
|
||||||
if status_as_string == "CLOSED":
|
|
||||||
return TournamentStatus.CLOSED
|
|
||||||
elif status_as_string == "OPEN":
|
|
||||||
return TournamentStatus.OPEN
|
|
||||||
elif status_as_string == "COMPLETED":
|
|
||||||
return TournamentStatus.COMPLETED
|
|
||||||
elif status_as_string == "CANCELED":
|
|
||||||
return TournamentStatus.CANCELED
|
|
||||||
elif status_as_string == "INVITE_ONLY":
|
|
||||||
return TournamentStatus.INVITE_ONLY
|
|
||||||
elif status_as_string == "ONGOING":
|
|
||||||
return TournamentStatus.ONGOING
|
|
||||||
else:
|
|
||||||
# If this happens, database is FUBAR
|
|
||||||
raise RuntimeError(f"Unknown TournamentStatus: {status_as_string}")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _parse_participant_type(participant_type_as_string: str) -> ParticipantType:
|
|
||||||
if participant_type_as_string == "PLAYER":
|
|
||||||
return ParticipantType.PLAYER
|
|
||||||
elif participant_type_as_string == "TEAM":
|
|
||||||
return ParticipantType.TEAM
|
|
||||||
else:
|
|
||||||
# If this happens, database is FUBAR
|
|
||||||
raise RuntimeError(f"Unknown ParticipantType: {participant_type_as_string}")
|
|
||||||
|
|
||||||
async def get_user_by_name(self, user_name: str) -> Optional[User]:
|
async def get_user_by_name(self, user_name: str) -> Optional[User]:
|
||||||
async with self._connection_pool.acquire() as conn:
|
async with self._connection_pool.acquire() as conn:
|
||||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||||
@@ -187,10 +133,10 @@ class DatabaseService:
|
|||||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||||
try:
|
try:
|
||||||
await cursor.execute(
|
await cursor.execute(
|
||||||
"UPDATE users SET user_name=%s, user_mail=%s, user_password=%s, user_fallback_password=%s,"
|
"UPDATE users SET user_name=%s, user_mail=%s, user_password=%s, user_first_name=%s, "
|
||||||
"user_first_name=%s, user_last_name=%s, user_birth_date=%s, is_active=%s, is_team_member=%s,"
|
"user_last_name=%s, user_birth_date=%s, is_active=%s, is_team_member=%s, is_admin=%s "
|
||||||
" is_admin=%s WHERE (user_id=%s)",
|
"WHERE (user_id=%s)",
|
||||||
(user.user_name, user.user_mail.lower(), user.user_password, user.user_fallback_password,
|
(user.user_name, user.user_mail.lower(), user.user_password,
|
||||||
user.user_first_name, user.user_last_name, user.user_birth_day,
|
user.user_first_name, user.user_last_name, user.user_birth_day,
|
||||||
user.is_active, user.is_team_member, user.is_admin,
|
user.is_active, user.is_team_member, user.is_admin,
|
||||||
user.user_id)
|
user.user_id)
|
||||||
@@ -286,6 +232,7 @@ class DatabaseService:
|
|||||||
except aiomysql.InterfaceError:
|
except aiomysql.InterfaceError:
|
||||||
pool_init_result = await self.init_db_pool()
|
pool_init_result = await self.init_db_pool()
|
||||||
if not pool_init_result:
|
if not pool_init_result:
|
||||||
|
print(self._connection_pool)
|
||||||
raise NoDatabaseConnectionError
|
raise NoDatabaseConnectionError
|
||||||
return await self.get_news(dt_start, dt_end)
|
return await self.get_news(dt_start, dt_end)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -449,7 +396,7 @@ class DatabaseService:
|
|||||||
pool_init_result = await self.init_db_pool()
|
pool_init_result = await self.init_db_pool()
|
||||||
if not pool_init_result:
|
if not pool_init_result:
|
||||||
raise NoDatabaseConnectionError
|
raise NoDatabaseConnectionError
|
||||||
return await self.delete_ticket(ticket_id)
|
return await self.change_ticket_owner(ticket_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error deleting ticket: {e}")
|
logger.warning(f"Error deleting ticket: {e}")
|
||||||
return False
|
return False
|
||||||
@@ -841,347 +788,3 @@ class DatabaseService:
|
|||||||
return await self.remove_profile_picture(user_id)
|
return await self.remove_profile_picture(user_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error deleting user profile picture: {e}")
|
logger.warning(f"Error deleting user profile picture: {e}")
|
||||||
|
|
||||||
async def get_all_tournaments(self) -> list[Tournament]:
|
|
||||||
logger.info(f"Polling Tournaments...")
|
|
||||||
async with self._connection_pool.acquire() as conn:
|
|
||||||
async with conn.cursor(aiomysql.DictCursor) as cursor:
|
|
||||||
try:
|
|
||||||
await cursor.execute(
|
|
||||||
"""
|
|
||||||
SELECT
|
|
||||||
/* =======================
|
|
||||||
Tournament
|
|
||||||
======================= */
|
|
||||||
t.id AS tournament_id,
|
|
||||||
t.name AS tournament_name,
|
|
||||||
t.description AS tournament_description,
|
|
||||||
t.format AS tournament_format,
|
|
||||||
t.start_time,
|
|
||||||
t.status AS tournament_status,
|
|
||||||
t.max_participants,
|
|
||||||
t.created_at,
|
|
||||||
t.participant_type AS tournament_participant_type,
|
|
||||||
|
|
||||||
/* =======================
|
|
||||||
Game Title
|
|
||||||
======================= */
|
|
||||||
gt.id AS game_title_id,
|
|
||||||
gt.name AS game_title_name,
|
|
||||||
gt.description AS game_title_description,
|
|
||||||
gt.web_link AS game_title_web_link,
|
|
||||||
gt.image_name AS game_title_image_name,
|
|
||||||
|
|
||||||
/* =======================
|
|
||||||
Tournament Participant
|
|
||||||
======================= */
|
|
||||||
tp.id AS participant_id,
|
|
||||||
tp.user_id,
|
|
||||||
tp.team_id,
|
|
||||||
tp.participant_type,
|
|
||||||
tp.seed,
|
|
||||||
tp.joined_at
|
|
||||||
|
|
||||||
FROM tournaments t
|
|
||||||
JOIN game_titles gt
|
|
||||||
ON gt.id = t.game_title_id
|
|
||||||
|
|
||||||
LEFT JOIN tournament_participants tp
|
|
||||||
ON tp.tournament_id = t.id
|
|
||||||
|
|
||||||
ORDER BY
|
|
||||||
t.id,
|
|
||||||
tp.seed IS NULL,
|
|
||||||
tp.seed;
|
|
||||||
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
await conn.commit()
|
|
||||||
except aiomysql.InterfaceError:
|
|
||||||
pool_init_result = await self.init_db_pool()
|
|
||||||
if not pool_init_result:
|
|
||||||
raise NoDatabaseConnectionError
|
|
||||||
return await self.get_all_tournaments()
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error getting tournaments: {e}")
|
|
||||||
|
|
||||||
tournaments = []
|
|
||||||
current_tournament: Optional[Tournament] = None
|
|
||||||
for row in await cursor.fetchall():
|
|
||||||
if current_tournament is None or current_tournament.id != row["tournament_id"]:
|
|
||||||
if current_tournament is not None:
|
|
||||||
tournaments.append(current_tournament)
|
|
||||||
participant_type = self._parse_participant_type(row["tournament_participant_type"])
|
|
||||||
id_accessor = "user_id" if participant_type == ParticipantType.PLAYER else "team_id"
|
|
||||||
current_tournament = Tournament(
|
|
||||||
id_=row["tournament_id"],
|
|
||||||
name=row["tournament_name"],
|
|
||||||
description=row["tournament_description"],
|
|
||||||
game_title=GameTitle(
|
|
||||||
name=row["game_title_name"],
|
|
||||||
description=row["game_title_description"],
|
|
||||||
web_link=row["game_title_web_link"],
|
|
||||||
image_name=row["game_title_image_name"]
|
|
||||||
),
|
|
||||||
format_=self._parse_tournament_format(row["tournament_format"]),
|
|
||||||
start_time=row["start_time"],
|
|
||||||
status=self._parse_tournament_status(row["tournament_status"]),
|
|
||||||
participants=[Participant(id_=row[id_accessor], participant_type=self._parse_participant_type(row["participant_type"]))] if row[id_accessor] is not None else [],
|
|
||||||
matches=None, # ToDo: Implement
|
|
||||||
rounds=[], # ToDo: Implement
|
|
||||||
max_participants=row["max_participants"],
|
|
||||||
participant_type=participant_type
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
id_accessor = "user_id" if current_tournament.participant_type == ParticipantType.PLAYER else "team_id"
|
|
||||||
current_tournament.add_participant(
|
|
||||||
Participant(id_=row[id_accessor], participant_type=self._parse_participant_type(row["participant_type"]))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
tournaments.append(current_tournament)
|
|
||||||
|
|
||||||
return tournaments
|
|
||||||
|
|
||||||
async def add_participant_to_tournament(self, participant: Participant, tournament: Tournament) -> None:
|
|
||||||
if participant.participant_type != tournament.participant_type:
|
|
||||||
raise ValueError(f"Can not add {participant.participant_type.name} to {tournament.participant_type.name} tournament")
|
|
||||||
accessor = "user_id" if participant.participant_type == ParticipantType.PLAYER else "team_id"
|
|
||||||
async with self._connection_pool.acquire() as conn:
|
|
||||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
|
||||||
try:
|
|
||||||
await cursor.execute(
|
|
||||||
f"INSERT INTO tournament_participants (tournament_id, {accessor}, participant_type) VALUES (%s, %s, %s);",
|
|
||||||
(tournament.id, participant.id, participant.participant_type.name)
|
|
||||||
)
|
|
||||||
await conn.commit()
|
|
||||||
except aiomysql.InterfaceError:
|
|
||||||
pool_init_result = await self.init_db_pool()
|
|
||||||
if not pool_init_result:
|
|
||||||
raise NoDatabaseConnectionError
|
|
||||||
return await self.add_participant_to_tournament(participant, tournament)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error adding participant to tournament: {e}")
|
|
||||||
|
|
||||||
async def remove_participant_from_tournament(self, participant: Participant, tournament: Tournament) -> None:
|
|
||||||
accessor = "user_id" if participant.participant_type == ParticipantType.PLAYER else "team_id"
|
|
||||||
async with self._connection_pool.acquire() as conn:
|
|
||||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
|
||||||
try:
|
|
||||||
await cursor.execute(
|
|
||||||
f"DELETE FROM tournament_participants WHERE (tournament_id = %s AND {accessor} = %s);",
|
|
||||||
(tournament.id, participant.id)
|
|
||||||
)
|
|
||||||
await conn.commit()
|
|
||||||
except aiomysql.InterfaceError:
|
|
||||||
pool_init_result = await self.init_db_pool()
|
|
||||||
if not pool_init_result:
|
|
||||||
raise NoDatabaseConnectionError
|
|
||||||
return await self.remove_participant_from_tournament(participant, tournament)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error removing participant from tournament: {e}")
|
|
||||||
|
|
||||||
async def get_teams(self) -> list[Team]:
|
|
||||||
async with self._connection_pool.acquire() as conn:
|
|
||||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
|
||||||
query = """
|
|
||||||
SELECT
|
|
||||||
t.id AS team_id,
|
|
||||||
t.name AS team_name,
|
|
||||||
t.abbreviation AS team_abbr,
|
|
||||||
t.join_password,
|
|
||||||
t.created_at AS team_created_at,
|
|
||||||
|
|
||||||
tm.status AS team_status,
|
|
||||||
tm.joined_at AS member_joined_at,
|
|
||||||
|
|
||||||
u.*
|
|
||||||
|
|
||||||
FROM teams t
|
|
||||||
|
|
||||||
LEFT JOIN team_members tm
|
|
||||||
ON t.id = tm.team_id
|
|
||||||
|
|
||||||
LEFT JOIN users u
|
|
||||||
ON tm.user_id = u.user_id
|
|
||||||
|
|
||||||
ORDER BY
|
|
||||||
t.id,
|
|
||||||
CASE tm.status
|
|
||||||
WHEN 'LEADER' THEN 1
|
|
||||||
WHEN 'OFFICER' THEN 2
|
|
||||||
WHEN 'MEMBER' THEN 3
|
|
||||||
ELSE 4
|
|
||||||
END,
|
|
||||||
u.user_name;
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
await cursor.execute(query)
|
|
||||||
await conn.commit()
|
|
||||||
except aiomysql.InterfaceError:
|
|
||||||
pool_init_result = await self.init_db_pool()
|
|
||||||
if not pool_init_result:
|
|
||||||
raise NoDatabaseConnectionError
|
|
||||||
return await self.get_teams()
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error getting teams: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
current_team: Optional[Team] = None
|
|
||||||
all_teams = []
|
|
||||||
|
|
||||||
for row in await cursor.fetchall():
|
|
||||||
if row[5] is None: # Teams without single member are ignored
|
|
||||||
continue
|
|
||||||
if current_team is None:
|
|
||||||
user = self._map_db_result_to_user(row[7:])
|
|
||||||
current_team = Team(id=row[0], name=row[1], abbreviation=row[2], join_password=row[3], members={user: TeamStatus.from_str(row[5])})
|
|
||||||
elif current_team.id == row[0]: # Still same team
|
|
||||||
current_team.members[self._map_db_result_to_user(row[7:])] = TeamStatus.from_str(row[5])
|
|
||||||
else:
|
|
||||||
all_teams.append(current_team)
|
|
||||||
user = self._map_db_result_to_user(row[7:])
|
|
||||||
current_team = Team(id=row[0], name=row[1], abbreviation=row[2], join_password=row[3], members={user: TeamStatus.from_str(row[5])})
|
|
||||||
|
|
||||||
if current_team is not None:
|
|
||||||
all_teams.append(current_team)
|
|
||||||
|
|
||||||
return all_teams
|
|
||||||
|
|
||||||
async def get_team_by_id(self, team_id: int) -> Optional[Team]:
|
|
||||||
async with self._connection_pool.acquire() as conn:
|
|
||||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
|
||||||
query = """
|
|
||||||
SELECT
|
|
||||||
t.id AS team_id,
|
|
||||||
t.name AS team_name,
|
|
||||||
t.abbreviation AS team_abbr,
|
|
||||||
t.join_password,
|
|
||||||
t.created_at AS team_created_at,
|
|
||||||
|
|
||||||
tm.status AS team_status,
|
|
||||||
tm.joined_at AS member_joined_at,
|
|
||||||
|
|
||||||
u.*
|
|
||||||
|
|
||||||
FROM teams t
|
|
||||||
|
|
||||||
LEFT JOIN team_members tm
|
|
||||||
ON t.id = tm.team_id
|
|
||||||
|
|
||||||
LEFT JOIN users u
|
|
||||||
ON tm.user_id = u.user_id
|
|
||||||
|
|
||||||
WHERE t.id = %s
|
|
||||||
|
|
||||||
ORDER BY
|
|
||||||
t.id,
|
|
||||||
CASE tm.status
|
|
||||||
WHEN 'LEADER' THEN 1
|
|
||||||
WHEN 'OFFICER' THEN 2
|
|
||||||
WHEN 'MEMBER' THEN 3
|
|
||||||
ELSE 4
|
|
||||||
END,
|
|
||||||
u.user_name;
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
await cursor.execute(query, (team_id, ))
|
|
||||||
await conn.commit()
|
|
||||||
except aiomysql.InterfaceError:
|
|
||||||
pool_init_result = await self.init_db_pool()
|
|
||||||
if not pool_init_result:
|
|
||||||
raise NoDatabaseConnectionError
|
|
||||||
return await self.get_team_by_id(team_id)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error getting team: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
team: Optional[Team] = None
|
|
||||||
|
|
||||||
for row in await cursor.fetchall():
|
|
||||||
if team is None:
|
|
||||||
user = self._map_db_result_to_user(row[7:])
|
|
||||||
team = Team(id=row[0], name=row[1], abbreviation=row[2], join_password=row[3], members={user: TeamStatus.from_str(row[5])})
|
|
||||||
elif team.id == row[0]:
|
|
||||||
team.members[self._map_db_result_to_user(row[7:])] = TeamStatus.from_str(row[5])
|
|
||||||
|
|
||||||
return team
|
|
||||||
|
|
||||||
async def create_team(self, team_name: str, team_abbr: str, join_password: str, leader: User) -> Team:
|
|
||||||
async with self._connection_pool.acquire() as conn:
|
|
||||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
|
||||||
try:
|
|
||||||
await cursor.execute(
|
|
||||||
"INSERT INTO teams (name, abbreviation, join_password) "
|
|
||||||
"VALUES (%s, %s, %s)", (team_name, team_abbr, join_password)
|
|
||||||
)
|
|
||||||
await conn.commit()
|
|
||||||
team_id = cursor.lastrowid
|
|
||||||
await cursor.execute(
|
|
||||||
"INSERT INTO team_members (team_id, user_id, status) VALUES (%s, %s, %s)",
|
|
||||||
(team_id, leader.user_id, TeamStatus.LEADER.name)
|
|
||||||
)
|
|
||||||
await conn.commit()
|
|
||||||
return await self.get_team_by_id(team_id)
|
|
||||||
|
|
||||||
except aiomysql.InterfaceError:
|
|
||||||
pool_init_result = await self.init_db_pool()
|
|
||||||
if not pool_init_result:
|
|
||||||
raise NoDatabaseConnectionError
|
|
||||||
return await self.create_team(team_name, team_abbr, join_password)
|
|
||||||
except aiomysql.IntegrityError as e:
|
|
||||||
logger.warning(f"Aborted duplication entry: {e}")
|
|
||||||
raise DuplicationError
|
|
||||||
|
|
||||||
async def update_team(self, team: Team) -> Team:
|
|
||||||
async with self._connection_pool.acquire() as conn:
|
|
||||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
|
||||||
try:
|
|
||||||
await cursor.execute(
|
|
||||||
"UPDATE teams SET name = %s, abbreviation = %s, join_password = %s WHERE (id = %s)",
|
|
||||||
(team.name, team.abbreviation, team.join_password, team.id)
|
|
||||||
)
|
|
||||||
await conn.commit()
|
|
||||||
return await self.get_team_by_id(team.id)
|
|
||||||
except aiomysql.InterfaceError:
|
|
||||||
pool_init_result = await self.init_db_pool()
|
|
||||||
if not pool_init_result:
|
|
||||||
raise NoDatabaseConnectionError
|
|
||||||
return await self.update_team(team)
|
|
||||||
except aiomysql.IntegrityError as e:
|
|
||||||
logger.warning(f"Aborted duplication entry: {e}")
|
|
||||||
raise DuplicationError
|
|
||||||
|
|
||||||
|
|
||||||
async def add_member_to_team(self, team: Team, user: User, status: TeamStatus = TeamStatus.MEMBER) -> None:
|
|
||||||
async with self._connection_pool.acquire() as conn:
|
|
||||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
|
||||||
try:
|
|
||||||
await cursor.execute(
|
|
||||||
"INSERT INTO team_members (team_id, user_id, status) VALUES (%s, %s, %s)",
|
|
||||||
(team.id, user.user_id, status.name)
|
|
||||||
)
|
|
||||||
await conn.commit()
|
|
||||||
except aiomysql.InterfaceError:
|
|
||||||
pool_init_result = await self.init_db_pool()
|
|
||||||
if not pool_init_result:
|
|
||||||
raise NoDatabaseConnectionError
|
|
||||||
return await self.add_member_to_team(team, user, status)
|
|
||||||
except aiomysql.IntegrityError as e:
|
|
||||||
logger.warning(f"Failed to add member {user.user_name} to team {team.name}: {e}")
|
|
||||||
raise DuplicationError
|
|
||||||
|
|
||||||
|
|
||||||
async def remove_user_from_team(self, team: Team, user: User) -> None:
|
|
||||||
async with self._connection_pool.acquire() as conn:
|
|
||||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
|
||||||
try:
|
|
||||||
await cursor.execute(
|
|
||||||
"DELETE FROM team_members WHERE team_id = %s AND user_id = %s",
|
|
||||||
(team.id, user.user_id)
|
|
||||||
)
|
|
||||||
await conn.commit()
|
|
||||||
except aiomysql.InterfaceError:
|
|
||||||
pool_init_result = await self.init_db_pool()
|
|
||||||
if not pool_init_result:
|
|
||||||
raise NoDatabaseConnectionError
|
|
||||||
return await self.remove_user_from_team(team, user)
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import secrets
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from rio import UserSettings
|
||||||
|
|
||||||
|
from src.ez_lan_manager.types.SessionStorage import SessionStorage
|
||||||
|
|
||||||
|
|
||||||
|
class LocalData(UserSettings):
|
||||||
|
stored_session_token: Optional[str] = None
|
||||||
|
|
||||||
|
class LocalDataService:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._session: dict[str, SessionStorage] = {}
|
||||||
|
|
||||||
|
def verify_token(self, token: str) -> Optional[SessionStorage]:
|
||||||
|
return self._session.get(token)
|
||||||
|
|
||||||
|
def set_session(self, session: SessionStorage) -> str:
|
||||||
|
key = secrets.token_hex(32)
|
||||||
|
self._session[key] = session
|
||||||
|
return key
|
||||||
|
|
||||||
|
def del_session(self, token: str) -> None:
|
||||||
|
self._session.pop(token, None)
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
from decimal import Decimal
|
|
||||||
from email.message import EmailMessage
|
from email.message import EmailMessage
|
||||||
from asyncio import sleep
|
from asyncio import sleep
|
||||||
|
|
||||||
import aiosmtplib
|
import aiosmtplib
|
||||||
|
|
||||||
from src.ezgg_lan_manager.services.ConfigurationService import ConfigurationService
|
from src.ez_lan_manager.services.ConfigurationService import ConfigurationService
|
||||||
from src.ezgg_lan_manager.types.User import User
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
|
|
||||||
@@ -18,9 +16,6 @@ class MailingService:
|
|||||||
async def send_email(self, subject: str, body: str, receiver: str) -> None:
|
async def send_email(self, subject: str, body: str, receiver: str) -> None:
|
||||||
if self._configuration_service.DEV_MODE_ACTIVE:
|
if self._configuration_service.DEV_MODE_ACTIVE:
|
||||||
logger.info(f"Skipped sending mail to {receiver} because demo mode is active.")
|
logger.info(f"Skipped sending mail to {receiver} because demo mode is active.")
|
||||||
logger.info(f"Subject: {subject}")
|
|
||||||
logger.info(f"Receiver: {receiver}")
|
|
||||||
logger.info(f"Body: {body}")
|
|
||||||
await sleep(1)
|
await sleep(1)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -40,15 +35,3 @@ class MailingService:
|
|||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to send email: {e}")
|
logger.error(f"Failed to send email: {e}")
|
||||||
|
|
||||||
def generate_account_balance_added_mail_body(self, user: User, added_balance: Decimal, total_balance: Decimal) -> str:
|
|
||||||
return f"""
|
|
||||||
Hallo {user.user_name},
|
|
||||||
|
|
||||||
deinem Account wurden {added_balance:.2f} € hinzugefügt. Dein neues Guthaben beträgt nun {total_balance:.2f} €.
|
|
||||||
|
|
||||||
Wenn du zu dieser Aufladung Fragen hast, stehen wir dir in unserem Discord Server oder per Mail an {self._configuration_service.get_lan_info().organizer_mail} zur Verfügung.
|
|
||||||
|
|
||||||
Liebe Grüße
|
|
||||||
Dein {self._configuration_service.get_lan_info().name} Team
|
|
||||||
"""
|
|
||||||
@@ -2,8 +2,8 @@ import logging
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
from src.ez_lan_manager.services.DatabaseService import DatabaseService
|
||||||
from src.ezgg_lan_manager.types.News import News
|
from src.ez_lan_manager.types.News import News
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
|
|
||||||
@@ -2,10 +2,10 @@ import logging
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
from src.ez_lan_manager.services.DatabaseService import DatabaseService
|
||||||
from src.ezgg_lan_manager.services.TicketingService import TicketingService
|
from src.ez_lan_manager.services.TicketingService import TicketingService
|
||||||
from src.ezgg_lan_manager.types.ConfigurationTypes import LanInfo, SeatingConfiguration
|
from src.ez_lan_manager.types.ConfigurationTypes import LanInfo, SeatingConfiguration
|
||||||
from src.ezgg_lan_manager.types.Seat import Seat
|
from src.ez_lan_manager.types.Seat import Seat
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
|
|
||||||
@@ -22,7 +22,8 @@ class SeatAlreadyTakenError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
class SeatingService:
|
class SeatingService:
|
||||||
def __init__(self, lan_info: LanInfo, db_service: DatabaseService, ticketing_service: TicketingService) -> None:
|
def __init__(self, seating_configuration: SeatingConfiguration, lan_info: LanInfo, db_service: DatabaseService, ticketing_service: TicketingService) -> None:
|
||||||
|
self._seating_configuration = seating_configuration
|
||||||
self._lan_info = lan_info
|
self._lan_info = lan_info
|
||||||
self._db_service = db_service
|
self._db_service = db_service
|
||||||
self._ticketing_service = ticketing_service
|
self._ticketing_service = ticketing_service
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from src.ezgg_lan_manager.services.AccountingService import AccountingService, InsufficientFundsError
|
from src.ez_lan_manager.services.AccountingService import AccountingService, InsufficientFundsError
|
||||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
from src.ez_lan_manager.services.DatabaseService import DatabaseService
|
||||||
from src.ezgg_lan_manager.types.ConfigurationTypes import TicketInfo
|
from src.ez_lan_manager.types.ConfigurationTypes import TicketInfo
|
||||||
from src.ezgg_lan_manager.types.Ticket import Ticket
|
from src.ez_lan_manager.types.Ticket import Ticket
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
|
|
||||||
@@ -2,8 +2,8 @@ from hashlib import sha256
|
|||||||
from typing import Union, Optional
|
from typing import Union, Optional
|
||||||
from string import ascii_letters, digits
|
from string import ascii_letters, digits
|
||||||
|
|
||||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
from src.ez_lan_manager.services.DatabaseService import DatabaseService
|
||||||
from src.ezgg_lan_manager.types.User import User
|
from src.ez_lan_manager.types.User import User
|
||||||
|
|
||||||
|
|
||||||
class NameNotAllowedError(Exception):
|
class NameNotAllowedError(Exception):
|
||||||
@@ -47,8 +47,7 @@ class UserService:
|
|||||||
user_name = user_name.lower()
|
user_name = user_name.lower()
|
||||||
|
|
||||||
hashed_pw = sha256(password_clear_text.encode(encoding="utf-8")).hexdigest()
|
hashed_pw = sha256(password_clear_text.encode(encoding="utf-8")).hexdigest()
|
||||||
created_user = await self._db_service.create_user(user_name, user_mail, hashed_pw)
|
return await self._db_service.create_user(user_name, user_mail, hashed_pw)
|
||||||
return created_user
|
|
||||||
|
|
||||||
async def update_user(self, user: User) -> User:
|
async def update_user(self, user: User) -> User:
|
||||||
disallowed_char = self._check_for_disallowed_char(user.user_name)
|
disallowed_char = self._check_for_disallowed_char(user.user_name)
|
||||||
@@ -59,12 +58,9 @@ class UserService:
|
|||||||
|
|
||||||
async def is_login_valid(self, user_name_or_mail: str, password_clear_text: str) -> bool:
|
async def is_login_valid(self, user_name_or_mail: str, password_clear_text: str) -> bool:
|
||||||
user = await self.get_user(user_name_or_mail)
|
user = await self.get_user(user_name_or_mail)
|
||||||
user_password_hash = sha256(password_clear_text.encode(encoding="utf-8")).hexdigest()
|
|
||||||
if not user:
|
if not user:
|
||||||
return False
|
return False
|
||||||
if user.user_fallback_password and user.user_fallback_password == user_password_hash:
|
return user.user_password == sha256(password_clear_text.encode(encoding="utf-8")).hexdigest()
|
||||||
return True
|
|
||||||
return user.user_password == user_password_hash
|
|
||||||
|
|
||||||
|
|
||||||
def _check_for_disallowed_char(self, name: str) -> Optional[str]:
|
def _check_for_disallowed_char(self, name: str) -> Optional[str]:
|
||||||
@@ -4,8 +4,8 @@ from decimal import Decimal
|
|||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from typing import Optional, Iterable, Self
|
from typing import Optional, Iterable, Self
|
||||||
|
|
||||||
from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItem, CateringMenuItemCategory
|
from src.ez_lan_manager.types.CateringMenuItem import CateringMenuItem, CateringMenuItemCategory
|
||||||
from src.ezgg_lan_manager.types.User import User
|
from src.ez_lan_manager.types.User import User
|
||||||
|
|
||||||
CateringMenuItemsWithAmount = dict[CateringMenuItem, int]
|
CateringMenuItemsWithAmount = dict[CateringMenuItem, int]
|
||||||
|
|
||||||
@@ -48,10 +48,3 @@ class LanInfo:
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class SeatingConfiguration:
|
class SeatingConfiguration:
|
||||||
seats: dict[str, str]
|
seats: dict[str, str]
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ReceiptPrintingConfiguration:
|
|
||||||
host: str
|
|
||||||
port: int
|
|
||||||
order_print_endpoint: str
|
|
||||||
password: str
|
|
||||||
@@ -2,7 +2,7 @@ from dataclasses import dataclass
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from src.ezgg_lan_manager.types.User import User
|
from src.ez_lan_manager.types.User import User
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from src.ezgg_lan_manager.types.User import User
|
from src.ez_lan_manager.types.User import User
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import logging
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
|
|
||||||
|
|
||||||
|
# ToDo: Persist between reloads: https://rio.dev/docs/howto/persistent-settings
|
||||||
|
# Note for ToDo: rio.UserSettings are saved LOCALLY, do not just read a user_id here!
|
||||||
|
@dataclass(frozen=False)
|
||||||
|
class SessionStorage:
|
||||||
|
_user_id: Optional[int] = None # DEBUG: Put user ID here to skip login
|
||||||
|
_is_team_member: bool = False
|
||||||
|
_notification_callbacks: dict[str, Callable] = field(default_factory=dict)
|
||||||
|
|
||||||
|
async def clear(self) -> None:
|
||||||
|
await self.set_user_id_and_team_member_flag(None, False)
|
||||||
|
|
||||||
|
def subscribe_to_logged_in_or_out_event(self, component_id: str, callback: Callable) -> None:
|
||||||
|
self._notification_callbacks[component_id] = callback
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_id(self) -> Optional[int]:
|
||||||
|
return self._user_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_team_member(self) -> bool:
|
||||||
|
return self._is_team_member
|
||||||
|
|
||||||
|
async def set_user_id_and_team_member_flag(self, user_id: Optional[int], is_team_member: bool) -> None:
|
||||||
|
self._user_id = user_id
|
||||||
|
self._is_team_member = is_team_member
|
||||||
|
for component_id, callback in self._notification_callbacks.items():
|
||||||
|
logger.debug(f"Calling logged in callback from {component_id}")
|
||||||
|
await callback()
|
||||||
@@ -2,7 +2,7 @@ from dataclasses import dataclass
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from src.ezgg_lan_manager.types.User import User
|
from src.ez_lan_manager.types.User import User
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -9,7 +9,6 @@ class User:
|
|||||||
user_name: str
|
user_name: str
|
||||||
user_mail: str
|
user_mail: str
|
||||||
user_password: str
|
user_password: str
|
||||||
user_fallback_password: Optional[str]
|
|
||||||
user_first_name: Optional[str]
|
user_first_name: Optional[str]
|
||||||
user_last_name: Optional[str]
|
user_last_name: Optional[str]
|
||||||
user_birth_day: Optional[date]
|
user_birth_day: Optional[date]
|
||||||
@@ -20,9 +19,4 @@ class User:
|
|||||||
last_updated_at: datetime
|
last_updated_at: datetime
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
return hash(self.user_id)
|
return hash(f"{self.user_id}{self.user_name}{self.user_mail}")
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if not isinstance(other, User):
|
|
||||||
return NotImplemented
|
|
||||||
return self.user_id == other.user_id
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
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: 57 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |