Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 85619feed5 | |||
| 45ad5f164a | |||
| 4552b0dc86 | |||
| 0eb3757c84 |
-14
@@ -1,14 +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
|
|
||||||
EXPOSE 8090
|
|
||||||
EXPOSE 8091
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
|
||||||
@@ -1,29 +1,3 @@
|
|||||||
# ELM (EZGG LAN Manager)
|
# ELM
|
||||||
|
|
||||||
ELM is the successor of [EZGG LAN Manager](https://git.ezgg-ev.de/Vereins-IT/ezgg-lan-manager) with an overhauled UI and a document-oriented database instead of SQL.
|
The EZGG LAN Manager
|
||||||
|
|
||||||
# Development Setup
|
|
||||||
|
|
||||||
## Local
|
|
||||||
|
|
||||||
- Install Python requirements via `pip install -r requirements.txt`.
|
|
||||||
- Start A MongoDB (Version 8)
|
|
||||||
- Load clean dump via `mongorestore --uri="mongodb://localhost:27017" /path/to/clean_dump`
|
|
||||||
- Create user for DB
|
|
||||||
|
|
||||||
```js
|
|
||||||
use admin
|
|
||||||
|
|
||||||
db.createUser({
|
|
||||||
user: "elm_user",
|
|
||||||
pwd: "elm_password",
|
|
||||||
roles: [ { role: "root", db: "admin" } ]
|
|
||||||
})
|
|
||||||
```
|
|
||||||
- Edit config accordingly
|
|
||||||
|
|
||||||
## Docker
|
|
||||||
|
|
||||||
- Change `docker-compose.yaml` entrypoint as needed (remove `--public` and `--release` usually)
|
|
||||||
- Build and run `docker-compose.yaml`
|
|
||||||
- Load clean dump via `mongorestore --uri="mongodb://user:password@localhost:27017" /path/to/clean_dump`
|
|
||||||
+7
-13
@@ -9,14 +9,9 @@
|
|||||||
has_showers=false
|
has_showers=false
|
||||||
ts3_address=""
|
ts3_address=""
|
||||||
discord_invite_link=""
|
discord_invite_link=""
|
||||||
location_name=""
|
|
||||||
location_link=""
|
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
database_host="localhost"
|
db_address="mongodb://localhost:27017"
|
||||||
database_port="27017"
|
|
||||||
database_user="root"
|
|
||||||
database_password="password"
|
|
||||||
database_name="elm"
|
database_name="elm"
|
||||||
|
|
||||||
[mailing]
|
[mailing]
|
||||||
@@ -28,18 +23,18 @@
|
|||||||
|
|
||||||
[tickets]
|
[tickets]
|
||||||
[tickets."NORMAL"]
|
[tickets."NORMAL"]
|
||||||
total_tickets=38
|
total_tickets=24
|
||||||
price="25.00"
|
price="20.00"
|
||||||
description="Normales Ticket"
|
description="Normales Ticket"
|
||||||
additional_info="Berechtigt zur Nutzung eines regulären Platzes für die gesamte Dauer der LAN"
|
additional_info="Berechtigt zur Nutzung eines regulären Platzes für die gesamte Dauer der LAN"
|
||||||
can_be_sold=true
|
is_default=true
|
||||||
|
|
||||||
[tickets."DELUXE"]
|
[tickets."DELUXE"]
|
||||||
total_tickets=30
|
total_tickets=30
|
||||||
price="30.00"
|
price="25.00"
|
||||||
description="Deluxe Ticket"
|
description="Deluxe Ticket"
|
||||||
additional_info="Wie das normale Ticket, aber mit doppelt so breitem Tisch (160cm)"
|
additional_info="Wie das normale Ticket, aber mit doppelt so breitem Tisch (160cm)"
|
||||||
can_be_sold=true
|
is_default=false
|
||||||
|
|
||||||
[receipt_printing]
|
[receipt_printing]
|
||||||
host="10.0.0.103"
|
host="10.0.0.103"
|
||||||
@@ -48,9 +43,8 @@
|
|||||||
password="Alkohol1"
|
password="Alkohol1"
|
||||||
|
|
||||||
[misc]
|
[misc]
|
||||||
base_url="https://ezgg-lan.de" # In dev mode, this is localhost
|
|
||||||
default_profile_picture="src/elm/assets/img/anon.png"
|
default_profile_picture="src/elm/assets/img/anon.png"
|
||||||
dev_mode_active=true # Supresses E-Mail sending, activates PayPal sandbox API
|
dev_mode_active=true # Supresses E-Mail sending
|
||||||
|
|
||||||
[paypal]
|
[paypal]
|
||||||
client_id_sandbox=""
|
client_id_sandbox=""
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
services:
|
|
||||||
web:
|
|
||||||
build: .
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
environment:
|
|
||||||
PYTHONPATH: /opt/elm
|
|
||||||
ports:
|
|
||||||
- "8000:8000"
|
|
||||||
- "8001:8001"
|
|
||||||
volumes:
|
|
||||||
- ./:/opt/elm
|
|
||||||
entrypoint: ["/bin/sh", "-c", "cd /opt/elm/src && rio run --release --public --port 8000"]
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: mongo:8
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
environment:
|
|
||||||
MONGO_INITDB_ROOT_USERNAME: elm
|
|
||||||
MONGO_INITDB_ROOT_PASSWORD: elm
|
|
||||||
MONGO_INITDB_DATABASE: elm
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- "27017:27017"
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- database:/data/db
|
|
||||||
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
start_period: 15s
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
database:
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Clean Dump
|
|
||||||
|
|
||||||
Contents:
|
|
||||||
- Catering Menu Items
|
|
||||||
- Dev user (Typhus)
|
|
||||||
- Seats for the Donsbach plan
|
|
||||||
|
|
||||||
Import with `mongorestore --db elm path/to/clean_dump`
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"name":{"$numberInt":"1"}},"name":"name_1","unique":true}],"uuid":"44e7e14f0d2e4aaa8b9b99ed347f23fd","collectionName":"catering_menu_items","type":"collection"}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"4832101908374f838652eded40a1bc75","collectionName":"catering_orders","type":"collection"}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"ServerVersion":"8.0.23","ToolVersion":"100.17.0"}
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"7531022a88c143b4b6afa363e48c162a","collectionName":"seats","type":"collection"}
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"}],"uuid":"9a4862b6e9854a008a7970595887323c","collectionName":"tickets","type":"collection"}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"user_name":{"$numberInt":"1"}},"name":"user_name_1"}],"uuid":"7a12a5a29127409cadb8a500203fb97c","collectionName":"transactions","type":"collection"}
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_"},{"v":{"$numberInt":"2"},"key":{"user_name":{"$numberInt":"1"}},"name":"user_name_1","unique":true},{"v":{"$numberInt":"2"},"key":{"user_mail":{"$numberInt":"1"}},"name":"user_mail_1","unique":true}],"uuid":"95b3ec50ea054fd7b9ef0508b423a41d","collectionName":"users","type":"collection"}
|
|
||||||
Binary file not shown.
+3
-4
@@ -8,7 +8,7 @@ from from_root import from_root
|
|||||||
from rio import App, Theme, Color, Font, Icon, Session
|
from rio import App, Theme, Color, Font, Icon, Session
|
||||||
|
|
||||||
from elm.components.RootComponent import RootComponent
|
from elm.components.RootComponent import RootComponent
|
||||||
from elm.services import ConfigurationService, DatabaseService, UserService, LocalData, LocalDataService, MailingService, AccountingService, PreloadService, ReceiptPrintingService
|
from elm.services import ConfigurationService, DatabaseService, UserService, LocalData, LocalDataService, MailingService, AccountingService
|
||||||
from elm.types import UserSession
|
from elm.types import UserSession
|
||||||
|
|
||||||
logger = logging.getLogger("ELM")
|
logger = logging.getLogger("ELM")
|
||||||
@@ -48,7 +48,6 @@ Icon.register_single_icon(
|
|||||||
|
|
||||||
configuration_service = ConfigurationService(from_root("config.toml"))
|
configuration_service = ConfigurationService(from_root("config.toml"))
|
||||||
database_service = DatabaseService(configuration_service.get_database_configuration())
|
database_service = DatabaseService(configuration_service.get_database_configuration())
|
||||||
mailing_service = MailingService(configuration_service)
|
|
||||||
lan_info = configuration_service.get_lan_info()
|
lan_info = configuration_service.get_lan_info()
|
||||||
|
|
||||||
def is_mobile(self: Session) -> bool:
|
def is_mobile(self: Session) -> bool:
|
||||||
@@ -57,8 +56,8 @@ def is_mobile(self: Session) -> bool:
|
|||||||
Session.is_mobile = is_mobile
|
Session.is_mobile = is_mobile
|
||||||
|
|
||||||
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
|
||||||
if configuration_service.DEV_MODE_ACTIVE:
|
if configuration_service.DEV_MODE_ACTIVE:
|
||||||
# Use this line to fake being any user without having to log in
|
|
||||||
dev_user = await session[UserService].get_user("Typhus")
|
dev_user = await session[UserService].get_user("Typhus")
|
||||||
if not dev_user:
|
if not dev_user:
|
||||||
logger.fatal("DEV MODE USER DOES NOT EXIST")
|
logger.fatal("DEV MODE USER DOES NOT EXIST")
|
||||||
@@ -79,7 +78,7 @@ app = App(
|
|||||||
theme=theme,
|
theme=theme,
|
||||||
assets_dir=Path(__file__).parent / "assets",
|
assets_dir=Path(__file__).parent / "assets",
|
||||||
build=RootComponent,
|
build=RootComponent,
|
||||||
default_attachments=[LocalData(), configuration_service, database_service, UserService(), LocalDataService(), mailing_service, AccountingService(configuration_service, mailing_service), PreloadService(), ReceiptPrintingService(configuration_service.get_receipt_printing_configuration(), configuration_service.DEV_MODE_ACTIVE)],
|
default_attachments=[LocalData(), configuration_service, database_service, UserService(), LocalDataService(), MailingService(configuration_service), AccountingService(configuration_service.get_paypal_configuration())],
|
||||||
on_app_start=on_app_start,
|
on_app_start=on_app_start,
|
||||||
on_session_start=on_session_start,
|
on_session_start=on_session_start,
|
||||||
icon=from_root("src/elm/assets/img/favicon.png"),
|
icon=from_root("src/elm/assets/img/favicon.png"),
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# Preloaded content
|
|
||||||
|
|
||||||
The files in this directory are loaded upon app start, as they do not change during runtime.
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
Wie melde ich mich für die LAN an?
|
|
||||||
|
|
||||||
Registriere dich auf dieser Seite, lade dein Guthabenkonto auf und kaufe ein Ticket. Danach such dir einen freien Sitzplatz auf dem Sitzplan aus.
|
|
||||||
|
|
||||||
Wie lade ich mein Guthabenkonto auf?
|
|
||||||
|
|
||||||
Logge dich in deinen Account ein und klicke oben rechts auf dein Profilbild, von dort auf die Schaltfläche 'Guthaben' in der Navigationsleiste. Dort findest du alle weiteren Informationen.
|
|
||||||
|
|
||||||
Wie kann ich mein Ticket stornieren?
|
|
||||||
|
|
||||||
Schreibe uns eine Mail an tech@ezgg-ev.de, wir kümmern uns dann Zeitnah um die Stornierung.
|
|
||||||
|
|
||||||
Was soll ich zur LAN mitbringen?
|
|
||||||
|
|
||||||
Deinen PC inklusive aller zugehörigen Geräte (Maus, Tastatur, Monitor, Headset), sowie aller Anschlusskabel. Wir empfehlen ein LAN Kabel von mindestens 5 Metern Länge mitzubringen. Des weiteren benötigste du eine Mehrfachsteckdose, da dir an deinem Platz nur ein einzelner Steckplatz zugewiesen wird.
|
|
||||||
|
|
||||||
Wohin mit technischen Problemen?
|
|
||||||
|
|
||||||
Melde dich einfach am Einlass bzw in der Orga-Ecke, wir helfen gerne weiter.
|
|
||||||
|
|
||||||
Wo entsorge ich meinen Müll?
|
|
||||||
|
|
||||||
Im gesamten Gebäude findest du Mülltüten/Mülleimer.
|
|
||||||
|
|
||||||
Darf ich Cannabis konsumieren?
|
|
||||||
|
|
||||||
Generell verbieten wir den Konsum von Cannabis nicht. Beachte aber die allgemeine Gesetzeslage und ziehe ggf. die Bubatzkarte zu Rat.
|
|
||||||
|
|
||||||
Gibt es einen Discord oder TeamSpeak?
|
|
||||||
|
|
||||||
Du kannst gerne unseren Vereins-TeamSpeak3-Server unter ts3.ezgg-ev.de nutzen. Den Link zum offiziellen Discord findest du in der Kopfleiste.
|
|
||||||
|
|
||||||
Wo bleibt mein Essen?
|
|
||||||
|
|
||||||
Vermutlich ist es auf dem Weg. Du kannst auf der Catering-Seite den Status deiner Bestellung überprüfen. Hast du Bedenken das sie verloren gegangen sein könnte, sprich ein Team-Mitglied an der Theke darauf an.
|
|
||||||
|
|
||||||
Wie lange dauert eine Aufladung per Überweißung?
|
|
||||||
|
|
||||||
In der Regel wird das Guthaben deinem Konto innerhalb von 2 bis 3 Werktagen gutgeschrieben. In Ausnahmefällen kann es bis zu 7 Tagen dauern.
|
|
||||||
|
|
||||||
Wie melde ich meinen Clan an?
|
|
||||||
|
|
||||||
Wenn in deiner Gruppe mehr als 3 Personen sind, dann schreib uns bitte eine Mail mit dem Betreff 'Gruppenticket' an tech@ezgg-ev.de. Schreibe uns dort die Nutzer-ID's sowie die Sitzplätze deiner Gruppe auf. Gehe sicher das jede Person in deiner Gruppe entweder bereits ein passendes Ticket besitzt oder über genug Guthaben verfügt um ein Ticket zu kaufen.
|
|
||||||
|
|
||||||
Wo kann ich schlafen?
|
|
||||||
|
|
||||||
Im Gebäude sind offizielle Schlafbereiche ausgewiesen. Solange du keine Zugangs-, Durchgangs-, oder Rettungswege blockierst, darfst du überall schlafen.
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"§1 Allgemeine Bestimmungen": [
|
|
||||||
"Die Veranstaltung wird von der Einfach Zocken Gaming Gesellschaft e.V. organisiert.",
|
|
||||||
"Unser Event verfolgt gemeinnützige Ziele und ist nicht auf Profit ausgerichtet. Die erhobenen Teilnahmebeiträge dienen lediglich der Kostendeckung. Überschüsse werden für die Organisation und Durchführung zukünftiger ähnlicher Veranstaltungen verwendet.",
|
|
||||||
"Die Organisatoren haben das Recht, unerwünschte oder störende Personen jederzeit von der Veranstaltung auszuschließen (siehe §3). Im Falle eines Ausschlusses aufgrund eines Regelverstoßes erfolgt keine Rückerstattung des Eintrittspreises."
|
|
||||||
],
|
|
||||||
"§2 Teilnahmevoraussetzungen": [
|
|
||||||
"Die Teilnahme an der Veranstaltung ist nur Personen gestattet, die mindestens 18 Jahre alt sind. Ein amtlicher Altersnachweis ist erforderlich. Kann dieser Nachweis nicht erbracht werden, wird der Zugang zur Veranstaltung verweigert.",
|
|
||||||
"Jeder Teilnehmer muss die Teilnahmegebühr entrichtet haben und dies auf Anfrage nachweisen können. Mit der Bezahlung des Eintrittspreises erhält der Teilnehmer einen garantierten Platz auf der Veranstaltung.",
|
|
||||||
"Alle Teilnehmer sind verpflichtet, vor der Veranstaltung sicherheitsrelevante Patches und Updates für Betriebssysteme und Spiele einzuspielen. Es wird nicht garantiert, dass diese während der Veranstaltung heruntergeladen werden können."
|
|
||||||
],
|
|
||||||
"§3 Verhaltensregeln": [
|
|
||||||
"Innerhalb des Veranstaltungsgebäudes gilt ein striktes Rauchverbot.",
|
|
||||||
"Jeder Teilnehmer verpflichtet sich, während der Veranstaltung keine illegalen Handlungen durchzuführen.",
|
|
||||||
"Die unautorisierte Verbreitung von urheberrechtlich geschütztem Material ist strengstens untersagt.",
|
|
||||||
"Der Veranstalter übernimmt keine Haftung für Schäden an Geräten oder Daten der Teilnehmer, es sei denn, der Veranstalter oder seine Erfüllungsgehilfen haben die Schäden vorsätzlich oder grob fahrlässig verursacht. Ebenso wird keine Haftung bei Diebstahl oder Verlust persönlicher Gegenstände übernommen.",
|
|
||||||
"Teilnehmer dürfen den Ablauf der Veranstaltung nicht absichtlich stören, insbesondere nicht den Betrieb des Computer- und Stromnetzwerks. Als absichtliche Störung zählt auch die Nutzung von Software, die dem Spieler einen unfairen Vorteil verschafft (z.B. Cheats, Hacks) sowie das Ausnutzen von Bugs in Spielen, um einen Vorteil zu erzielen. Solche Verstöße führen zum sofortigen Ausschluss aus allen Turnieren. Betrifft der Verstoß ein Teammitglied, wird das gesamte Team disqualifiziert, auch wenn die anderen Mitglieder nicht direkt beteiligt waren. Wiederholte oder schwerwiegende Verstöße können zum Ausschluss von der gesamten Veranstaltung führen.",
|
|
||||||
"Die Nutzung von Aktivlautsprechern ist verboten, Kopfhörer sind Pflicht.",
|
|
||||||
"Verursacht ein Teilnehmer Schäden, haftet er vollumfänglich für die entstehenden Kosten.",
|
|
||||||
"Teilnehmer sind dazu verpflichtet, nach der Veranstaltung ihren Platz aufzuräumen und persönliche Gegenstände mitzunehmen."
|
|
||||||
],
|
|
||||||
"§4 Internetzugang": [
|
|
||||||
"Der Veranstalter stellt während der Veranstaltung einen eingeschränkten Internetzugang zur Verfügung. Es wird jedoch keine Garantie für die Verfügbarkeit, Eignung oder Zuverlässigkeit des Zugangs übernommen. Der Veranstalter behält sich das Recht vor, den Zugang zeitweise oder vollständig einzuschränken oder zu sperren sowie bestimmte Dienste oder Websites zu blockieren.",
|
|
||||||
"Für alle über das Internet getätigten Aktivitäten, Datenübertragungen und Rechtsgeschäfte ist der Teilnehmer allein verantwortlich. Entstehende Kosten durch die Nutzung von Drittanbieterdiensten trägt der Teilnehmer. Es gilt das Einhalten der gesetzlichen Bestimmungen.",
|
|
||||||
"Der Teilnehmer stellt den Veranstalter von jeglichen Ansprüchen Dritter frei, die aus einer rechtswidrigen Nutzung des Internetzugangs oder einem Verstoß gegen diese Vereinbarung resultieren. Diese Freistellung schließt auch die Kosten für die Abwehr solcher Ansprüche ein.",
|
|
||||||
"Der Veranstalter behält sich das Recht vor, die Nutzung des Internetzugangs zu protokollieren, um im Bedarfsfall Beweise für die Nutzung durch bestimmte Teilnehmer vorzulegen und den Veranstalter vor Schäden zu schützen."
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
Angaben gemäß § 5 TMG:
|
|
||||||
|
|
||||||
Einfach Zocken Gaming Gesellschaft e.V.
|
|
||||||
Im Elchgrund 18
|
|
||||||
35080 Bad Endbach - Bottenhorn
|
|
||||||
|
|
||||||
Vertreten durch:
|
|
||||||
|
|
||||||
1. Vorsitzender: David Rodenkirchen
|
|
||||||
2. Vorsitzender: Julia Albring
|
|
||||||
Schatzmeisterin: Jessica Rodenkirchen
|
|
||||||
|
|
||||||
Kontakt:
|
|
||||||
|
|
||||||
E-Mail: vorstand (at) ezgg-ev.de
|
|
||||||
|
|
||||||
Registereintrag:
|
|
||||||
|
|
||||||
Eingetragen im Vereinsregister.
|
|
||||||
Registergericht: Amtsgericht Marburg
|
|
||||||
Registernummer: VR 5837
|
|
||||||
|
|
||||||
Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV:
|
|
||||||
|
|
||||||
David Rodenkirchen
|
|
||||||
Im Elchgrund 18
|
|
||||||
35080 Bad Endbach - Bottenhorn
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
Respektvolles Verhalten: Sei höflich und respektvoll gegenüber anderen Gästen und dem Team.
|
|
||||||
Alkohol und Drogen: Konsumiere Alkohol in Maßen und halte dich an die gültige Gesetzeslage.
|
|
||||||
Sitzplätze: Respektiere die zugewiesenen Plätze und ändere sie nicht ohne Genehmigung.
|
|
||||||
Notausgänge und Sicherheitsvorschriften: Informiere dich über die Notausgänge und beachte die Sicherheitsanweisungen.
|
|
||||||
Müllentsorgung: Benutze die vorgesehenen Mülleimer und halte den Veranstaltungsort sauber.
|
|
||||||
Rauchen: Halte dich an die Rauchverbote und benutze nur die ausgewiesenen Raucherbereiche.
|
|
||||||
Hausrecht: Folge den Anweisungen des Veranstalters und des Sicherheitspersonals.
|
|
||||||
Illegales: Das brechen des deutschen Rechts, insbesondere des Urheberrechts, bleibt auch auf LAN verboten.
|
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from bson import ObjectId
|
|
||||||
from rio import Component, Rectangle, Column, Text, Row, PointerEventListener, TextInput
|
from rio import Component, Rectangle, Column, Text, Row, PointerEventListener, TextInput
|
||||||
from rio.event import on_populate
|
from rio.event import on_populate
|
||||||
|
|
||||||
from elm.types import UserSession, Ticket, Seat, User
|
from elm.types import UserSession
|
||||||
from elm.components import ElmButton
|
from elm.components import ElmButton
|
||||||
from elm.services import UserService
|
from elm.services import UserService
|
||||||
|
|
||||||
@@ -16,27 +13,14 @@ class AccountInfoBox(Component):
|
|||||||
account_info_is_error: bool = False
|
account_info_is_error: bool = False
|
||||||
password_input_blocked: bool = False
|
password_input_blocked: bool = False
|
||||||
password_change_in_progress: bool = False
|
password_change_in_progress: bool = False
|
||||||
ticket: Optional[Ticket] = None
|
|
||||||
seat: Optional[Seat] = None
|
|
||||||
fixed_user: Optional[User] = None
|
|
||||||
|
|
||||||
@on_populate
|
@on_populate
|
||||||
async def on_populate(self) -> None:
|
async def on_populate(self) -> None:
|
||||||
if self.fixed_user is None:
|
user = await self.session[UserService].get_user(self.session[UserSession].user_name)
|
||||||
try:
|
if user:
|
||||||
user = await self.session[UserService].get_user(self.session[UserSession].user_name)
|
self.mail = user.user_mail
|
||||||
if user:
|
|
||||||
self.mail = user.user_mail
|
|
||||||
self.ticket = await Ticket.find_one({"owner.$id": user.id})
|
|
||||||
self.seat = await Seat.find_one({"user.$id": ObjectId(user.id)})
|
|
||||||
else:
|
|
||||||
self.session.navigate_to("./login")
|
|
||||||
except KeyError:
|
|
||||||
self.session.navigate_to("./login")
|
|
||||||
else:
|
else:
|
||||||
self.mail = self.fixed_user.user_mail
|
self.session.navigate_to("./login")
|
||||||
self.ticket = await Ticket.find_one({"owner.$id": self.fixed_user.id})
|
|
||||||
self.seat = await Seat.find_one({"user.$id": ObjectId(self.fixed_user.id)})
|
|
||||||
|
|
||||||
async def set_new_password(self) -> None:
|
async def set_new_password(self) -> None:
|
||||||
self.password_change_in_progress = True
|
self.password_change_in_progress = True
|
||||||
@@ -49,11 +33,7 @@ class AccountInfoBox(Component):
|
|||||||
self.password_change_in_progress = False
|
self.password_change_in_progress = False
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.fixed_user is not None:
|
user = await self.session[UserService].get_user(self.session[UserSession].user_name)
|
||||||
user = self.fixed_user
|
|
||||||
else:
|
|
||||||
user = await self.session[UserService].get_user(self.session[UserSession].user_name)
|
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
self.account_info_is_error = True
|
self.account_info_is_error = True
|
||||||
self.account_info_text = "Unbekannter Fehler"
|
self.account_info_text = "Unbekannter Fehler"
|
||||||
@@ -75,12 +55,6 @@ class AccountInfoBox(Component):
|
|||||||
|
|
||||||
def build(self) -> Component:
|
def build(self) -> Component:
|
||||||
row_col = Row
|
row_col = Row
|
||||||
ticket_text = "-"
|
|
||||||
seat_text = "-"
|
|
||||||
if self.ticket:
|
|
||||||
ticket_text = self.ticket.category
|
|
||||||
if self.seat:
|
|
||||||
seat_text = self.seat.seat_id
|
|
||||||
if self.session.is_mobile():
|
if self.session.is_mobile():
|
||||||
row_col = Column
|
row_col = Column
|
||||||
|
|
||||||
@@ -96,18 +70,18 @@ class AccountInfoBox(Component):
|
|||||||
stroke_color=self.session.theme.box_border_color,
|
stroke_color=self.session.theme.box_border_color,
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
TextInput(text=self.fixed_user.user_name if self.fixed_user is not None else self.session[UserSession].user_name, label="Nutzername", is_sensitive=False),
|
TextInput(text=self.session[UserSession].user_name, label="Nutzername", is_sensitive=False),
|
||||||
TextInput(text=self.mail, label="E-Mail Adresse", is_sensitive=False),
|
TextInput(text=self.mail, label="E-Mail Adresse", is_sensitive=False),
|
||||||
row_col(
|
row_col(
|
||||||
PointerEventListener(
|
PointerEventListener(
|
||||||
Rectangle(
|
Rectangle(
|
||||||
content=Row(Text("Ticket:", margin=1, overflow="wrap", justify="left"), Text(ticket_text, margin=1, overflow="wrap", justify="right")),
|
content=Row(Text("Ticket:", margin=1, overflow="wrap", justify="left"), Text("-", margin=1, overflow="wrap", justify="right")),
|
||||||
fill=self.session.theme.success_color if self.ticket else self.session.theme.danger_color_dark,
|
fill=self.session.theme.danger_color_dark,
|
||||||
stroke_width=0.1,
|
stroke_width=0.1,
|
||||||
stroke_color=self.session.theme.success_color if self.ticket else self.session.theme.danger_color,
|
stroke_color=self.session.theme.danger_color,
|
||||||
hover_fill=self.session.theme.success_color if self.ticket else self.session.theme.danger_color,
|
hover_fill=self.session.theme.danger_color,
|
||||||
hover_stroke_width=0.1,
|
hover_stroke_width=0.1,
|
||||||
hover_stroke_color=self.session.theme.success_color if self.ticket else self.session.theme.danger_color_dark,
|
hover_stroke_color=self.session.theme.danger_color_dark,
|
||||||
transition_time=0.2,
|
transition_time=0.2,
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
),
|
),
|
||||||
@@ -115,13 +89,13 @@ class AccountInfoBox(Component):
|
|||||||
),
|
),
|
||||||
PointerEventListener(
|
PointerEventListener(
|
||||||
Rectangle(
|
Rectangle(
|
||||||
content=Row(Text("Sitzplatz:", margin=1, overflow="wrap", justify="left"), Text(seat_text, margin=1, overflow="wrap", justify="right")),
|
content=Row(Text("Sitzplatz:", margin=1, overflow="wrap", justify="left"), Text("-", margin=1, overflow="wrap", justify="right")),
|
||||||
fill=self.session.theme.success_color if self.seat else self.session.theme.danger_color_dark,
|
fill=self.session.theme.danger_color_dark,
|
||||||
stroke_width=0.1,
|
stroke_width=0.1,
|
||||||
stroke_color=self.session.theme.success_color if self.seat else self.session.theme.danger_color,
|
stroke_color=self.session.theme.danger_color,
|
||||||
hover_fill=self.session.theme.success_color if self.seat else self.session.theme.danger_color,
|
hover_fill=self.session.theme.danger_color,
|
||||||
hover_stroke_width=0.1,
|
hover_stroke_width=0.1,
|
||||||
hover_stroke_color=self.session.theme.success_color if self.seat else self.session.theme.danger_color_dark,
|
hover_stroke_color=self.session.theme.danger_color_dark,
|
||||||
transition_time=0.2,
|
transition_time=0.2,
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
from asyncio import sleep
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from rio import Component, Column, Row, Text, Spacer, Rectangle, ProgressBar, Tooltip
|
|
||||||
from rio.event import on_populate
|
|
||||||
|
|
||||||
from elm.services import AccountingService
|
|
||||||
from elm.components import ElmButton
|
|
||||||
from elm.services.AccountingService import InsufficientFundsError
|
|
||||||
from elm.types import Ticket, UserSession, User, TicketInfo, TicketState
|
|
||||||
|
|
||||||
|
|
||||||
class BuyTicketBox(Component):
|
|
||||||
ticket_info: TicketInfo
|
|
||||||
user_ticket: Optional[Ticket] = None
|
|
||||||
ticket_state: TicketState = TicketState.UNAVAILABLE
|
|
||||||
sold_tickets: int = 0
|
|
||||||
purchase_in_process: bool = False
|
|
||||||
purchase_status: str = ""
|
|
||||||
purchase_error_message: str = ""
|
|
||||||
|
|
||||||
@on_populate
|
|
||||||
async def on_populate(self) -> None:
|
|
||||||
self.sold_tickets = len(await Ticket.find_many(Ticket.category == self.ticket_info.category).to_list())
|
|
||||||
if self.sold_tickets >= self.ticket_info.total_tickets:
|
|
||||||
self.ticket_state = TicketState.SOLD_OUT
|
|
||||||
elif self.ticket_info.can_be_sold:
|
|
||||||
self.ticket_state = TicketState.AVAILABLE
|
|
||||||
else:
|
|
||||||
self.ticket_state = TicketState.UNAVAILABLE
|
|
||||||
self.user_ticket = await self.get_user_ticket()
|
|
||||||
|
|
||||||
async def get_user_ticket(self) -> Optional[Ticket]:
|
|
||||||
try:
|
|
||||||
user = await User.find_one(User.user_name == self.session[UserSession].user_name)
|
|
||||||
if not user:
|
|
||||||
return None
|
|
||||||
return await Ticket.find_one({"owner.$id": user.id})
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def is_logged_in(self) -> bool:
|
|
||||||
try:
|
|
||||||
return bool(self.session[UserSession].user_name)
|
|
||||||
except KeyError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_available_tickets(self) -> int:
|
|
||||||
return self.ticket_info.total_tickets - self.sold_tickets
|
|
||||||
|
|
||||||
async def buy_ticket(self) -> None:
|
|
||||||
self.purchase_in_process = True
|
|
||||||
self.purchase_status = "Ticket wird gekauft..."
|
|
||||||
await sleep(1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
user = await User.find_one(User.user_name == self.session[UserSession].user_name)
|
|
||||||
if not user:
|
|
||||||
raise KeyError
|
|
||||||
except KeyError:
|
|
||||||
self.session.navigate_to("./login")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.session[AccountingService].remove_balance(user.user_name, self.ticket_info.price, f"Ticketkauf - {self.ticket_info.category}")
|
|
||||||
except InsufficientFundsError:
|
|
||||||
self.purchase_in_process = False
|
|
||||||
self.purchase_status = ""
|
|
||||||
self.purchase_error_message = "Ungenügendes Guthaben!"
|
|
||||||
return
|
|
||||||
|
|
||||||
new_ticket = Ticket(category=self.ticket_info.category, purchase_date=datetime.now(), owner=user)
|
|
||||||
await new_ticket.save()
|
|
||||||
self.user_ticket = new_ticket
|
|
||||||
self.purchase_in_process = False
|
|
||||||
self.purchase_status = ""
|
|
||||||
self.sold_tickets = len(await Ticket.find_many(Ticket.category == self.ticket_info.category).to_list())
|
|
||||||
|
|
||||||
|
|
||||||
def build(self) -> Component:
|
|
||||||
ticket_owned_text = ""
|
|
||||||
if self.purchase_error_message:
|
|
||||||
button_row_content = Row(
|
|
||||||
Text(self.purchase_error_message, justify="center", margin=0.5, fill=self.session.theme.danger_color, overflow="wrap"),
|
|
||||||
)
|
|
||||||
elif self.purchase_in_process:
|
|
||||||
button_row_content = Row(
|
|
||||||
Text(self.purchase_status, justify="center", margin=0.5, overflow="wrap")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
if self.user_ticket:
|
|
||||||
button_row_content = Tooltip(anchor=ElmButton(text="Kaufen", is_disabled=True), tip="Du hast bereits ein Ticket")
|
|
||||||
ticket_owned_text = "Du besitzt dieses Ticket!" if self.user_ticket.category == self.ticket_info.category else ""
|
|
||||||
elif self.is_logged_in():
|
|
||||||
if self.ticket_state == TicketState.UNAVAILABLE:
|
|
||||||
button_row_content = Tooltip(anchor=ElmButton(text="Kaufen", is_disabled=True), tip="Aktuell nicht verfügbar")
|
|
||||||
elif self.ticket_state == TicketState.SOLD_OUT:
|
|
||||||
button_row_content = Tooltip(anchor=ElmButton(text="Kaufen", is_disabled=True), tip="Ausverkauft")
|
|
||||||
elif self.ticket_state == TicketState.AVAILABLE:
|
|
||||||
button_row_content = ElmButton(text="Kaufen", on_press=self.buy_ticket)
|
|
||||||
else:
|
|
||||||
button_row_content = Tooltip(anchor=ElmButton(text="Kaufen", is_disabled=True), tip="Entwickler hauen!")
|
|
||||||
else:
|
|
||||||
button_row_content = ElmButton(text="Kaufen", on_press=lambda: self.session.navigate_to("./login"))
|
|
||||||
return Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Row(
|
|
||||||
Text(self.ticket_info.description, margin=0.5, selectable=False, overflow="wrap", grow_x=True),
|
|
||||||
Text(self.session[AccountingService].make_euro_string_from_decimal(self.ticket_info.price), justify="right", margin_right=0.5, fill=self.session.theme.warning_color)
|
|
||||||
),
|
|
||||||
fill=self.session.theme.header_box_background_color,
|
|
||||||
margin=0.4
|
|
||||||
),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
Text(self.ticket_info.additional_info, overflow="wrap", margin_bottom=1),
|
|
||||||
Text(ticket_owned_text, margin_bottom=3, overflow="wrap", fill=self.session.theme.success_color),
|
|
||||||
Row(Text("Verfügbar:", font_size=0.8 if self.session.is_mobile() else 1), Text(f"{self.get_available_tickets()} / {self.ticket_info.total_tickets}", justify="right", font_size=0.8 if self.session.is_mobile() else 1)),
|
|
||||||
ProgressBar(progress=self.get_available_tickets() / self.ticket_info.total_tickets, min_height=1),
|
|
||||||
button_row_content,
|
|
||||||
margin=1,
|
|
||||||
spacing=1
|
|
||||||
),
|
|
||||||
Spacer()
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color
|
|
||||||
),
|
|
||||||
Spacer()
|
|
||||||
)
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
from decimal import Decimal
|
|
||||||
from functools import partial
|
|
||||||
|
|
||||||
from rio import Component, Rectangle, Column, Text, Spacer, List, Row, IconButton
|
|
||||||
|
|
||||||
from elm.components import ElmButton
|
|
||||||
from elm.services import AccountingService, ReceiptPrintingService
|
|
||||||
from elm.types import User, UserSession
|
|
||||||
from elm.types.CateringTypes import CateringOrderedItem, CateringOrder
|
|
||||||
|
|
||||||
|
|
||||||
class CateringCart(Component):
|
|
||||||
cart: List[CateringOrderedItem]
|
|
||||||
order_button_loading: bool = False
|
|
||||||
status_text: str = " "
|
|
||||||
status_is_error: bool = False
|
|
||||||
|
|
||||||
def remove_item(self, item: CateringOrderedItem) -> None:
|
|
||||||
self.cart.remove(item)
|
|
||||||
|
|
||||||
def get_cart_price(self) -> str:
|
|
||||||
price = Decimal(0)
|
|
||||||
for item in self.cart:
|
|
||||||
price += item.final_unit_price
|
|
||||||
return self.session[AccountingService].make_euro_string_from_decimal(price)
|
|
||||||
|
|
||||||
async def order_pressed(self) -> None:
|
|
||||||
self.order_button_loading = True
|
|
||||||
|
|
||||||
if len(self.cart) == 0:
|
|
||||||
self.status_text = "Warenkorb leer!"
|
|
||||||
self.status_is_error = True
|
|
||||||
self.order_button_loading = False
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
user = await User.find_one(User.user_name == self.session[UserSession].user_name)
|
|
||||||
except KeyError:
|
|
||||||
self.status_text = "Nicht eingeloggt!"
|
|
||||||
self.status_is_error = True
|
|
||||||
self.order_button_loading = False
|
|
||||||
return None
|
|
||||||
if not user:
|
|
||||||
self.status_text = "Nicht eingeloggt!"
|
|
||||||
self.status_is_error = True
|
|
||||||
self.order_button_loading = False
|
|
||||||
return None
|
|
||||||
|
|
||||||
total_price = Decimal(0)
|
|
||||||
for item in self.cart:
|
|
||||||
total_price += item.final_unit_price
|
|
||||||
|
|
||||||
balance = await self.session[AccountingService].get_balance(user.user_name)
|
|
||||||
|
|
||||||
if total_price > balance:
|
|
||||||
self.status_text = "Guthaben nicht ausreichend!"
|
|
||||||
self.status_is_error = True
|
|
||||||
self.order_button_loading = False
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
new_order = await CateringOrder(
|
|
||||||
customer_id=user.id,
|
|
||||||
items=list(self.cart)
|
|
||||||
).save()
|
|
||||||
except Exception as e:
|
|
||||||
self.status_text = f"Fehler: {e}"
|
|
||||||
self.status_is_error = True
|
|
||||||
self.order_button_loading = False
|
|
||||||
return None
|
|
||||||
|
|
||||||
await self.session[AccountingService].remove_balance(
|
|
||||||
user.user_name,
|
|
||||||
total_price,
|
|
||||||
f"Catering: {new_order.id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.cart.clear()
|
|
||||||
self.order_button_loading = False
|
|
||||||
self.status_text = "Bestellt!"
|
|
||||||
self.status_is_error = False
|
|
||||||
|
|
||||||
self.session.create_task(self.session[ReceiptPrintingService].print_order(user, new_order))
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def build(self) -> Component:
|
|
||||||
return Rectangle(
|
|
||||||
content=Column(
|
|
||||||
*[Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Row(
|
|
||||||
Text(text=item.name, overflow="ellipsize", grow_x=True),
|
|
||||||
IconButton(icon="material/cancel", style="plain-text", min_size=2, on_press=partial(self.remove_item, item)),
|
|
||||||
margin=0.5
|
|
||||||
),
|
|
||||||
*[
|
|
||||||
Row(Text(f"{'MIT' if modifier.selected else 'OHNE'} {modifier.label}", overflow="ellipsize", grow_x=True, font_size=0.8, margin_left=1), margin=0.5)
|
|
||||||
for modifier in item.selected_modifiers
|
|
||||||
],
|
|
||||||
Row(Text(self.session[AccountingService].make_euro_string_from_decimal(item.final_unit_price), overflow="ellipsize", grow_x=True, margin_left=1, justify="center"), margin=0.5),
|
|
||||||
),
|
|
||||||
margin=0.5,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.primary_color
|
|
||||||
) for item in self.cart],
|
|
||||||
Spacer(),
|
|
||||||
Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Row(
|
|
||||||
Text(text="Preis:", overflow="nowrap"),
|
|
||||||
Text(text=self.get_cart_price(), overflow="ellipsize", grow_x=True, justify="right")
|
|
||||||
),
|
|
||||||
ElmButton(text="Bestellen", is_loading=self.order_button_loading, on_press=self.order_pressed),
|
|
||||||
Text(text=self.status_text, fill=self.session.theme.danger_color if self.status_is_error else self.session.theme.success_color, overflow="wrap", justify="center"),
|
|
||||||
margin=0.5,
|
|
||||||
spacing=1
|
|
||||||
),
|
|
||||||
margin=0.5,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.primary_color
|
|
||||||
)
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color
|
|
||||||
)
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
from typing import Literal, Callable
|
|
||||||
|
|
||||||
from rio import Component, Rectangle, Column, Spacer, Text
|
|
||||||
from rio.event import on_populate
|
|
||||||
|
|
||||||
from elm.components import CateringItemBox
|
|
||||||
from elm.types.CateringTypes import CateringMenuItem, CateringMenuItemCategory
|
|
||||||
|
|
||||||
ITEM_CATEGORY_BY_DISPLAY_CATEGORY: dict[Literal["Frühstück", "Hauptspeisen", "Snacks & Dessert", "Softdrinks", "Alkohol"], list[CateringMenuItemCategory]] = {
|
|
||||||
"Frühstück": [CateringMenuItemCategory.BREAKFAST],
|
|
||||||
"Hauptspeisen": [CateringMenuItemCategory.MAIN_COURSE],
|
|
||||||
"Snacks & Dessert": [CateringMenuItemCategory.SNACK, CateringMenuItemCategory.DESSERT],
|
|
||||||
"Softdrinks": [CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC],
|
|
||||||
"Alkohol": [CateringMenuItemCategory.BEVERAGE_ALCOHOLIC, CateringMenuItemCategory.BEVERAGE_COCKTAIL, CateringMenuItemCategory.BEVERAGE_SHOT]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class CateringCategoryDisplay(Component):
|
|
||||||
active_category: Literal["Frühstück", "Hauptspeisen", "Snacks & Dessert", "Softdrinks", "Alkohol"]
|
|
||||||
add_to_cart_pressed_callback: Callable
|
|
||||||
catering_menu_items: list[CateringMenuItem] = []
|
|
||||||
|
|
||||||
@on_populate
|
|
||||||
async def on_populate(self) -> None:
|
|
||||||
self.catering_menu_items = await CateringMenuItem.find(
|
|
||||||
{
|
|
||||||
"category": {
|
|
||||||
"$in": ITEM_CATEGORY_BY_DISPLAY_CATEGORY[self.active_category]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).to_list()
|
|
||||||
|
|
||||||
async def add_to_cart_pressed(self, item: CateringMenuItem, changed_options: dict[str, bool]) -> None:
|
|
||||||
await self.add_to_cart_pressed_callback(item, changed_options)
|
|
||||||
|
|
||||||
def build(self) -> Component:
|
|
||||||
if len(self.catering_menu_items) <= 0:
|
|
||||||
return Spacer()
|
|
||||||
|
|
||||||
return Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Text(self.active_category, margin=0.5, selectable=False, overflow="wrap"),
|
|
||||||
fill=self.session.theme.header_box_background_color,
|
|
||||||
margin=0.4
|
|
||||||
),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
),
|
|
||||||
# Items here
|
|
||||||
Column(*[CateringItemBox(item=i, add_to_cart_pressed_callback=self.add_to_cart_pressed, margin=0.5, grow_y=True) for i in self.catering_menu_items]),
|
|
||||||
Spacer()
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color
|
|
||||||
)
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
from decimal import Decimal
|
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
from rio import Component, Rectangle, Column, Text, Row, Separator, Color, Checkbox, FlowContainer, Icon, Spacer, CheckboxChangeEvent, PointerEventListener, PointerEvent, Dict, TextStyle
|
|
||||||
|
|
||||||
from elm.services import AccountingService
|
|
||||||
from elm.types.CateringTypes import CateringMenuItem, CateringModificationKey, CateringModifierOption
|
|
||||||
|
|
||||||
|
|
||||||
class CateringItemBox(Component):
|
|
||||||
item: CateringMenuItem
|
|
||||||
add_to_cart_pressed_callback: Callable
|
|
||||||
changed_options: Dict[str, bool] = Dict()
|
|
||||||
|
|
||||||
def make_money_string(self, money: Decimal) -> str:
|
|
||||||
return self.session[AccountingService].make_euro_string_from_decimal(money)
|
|
||||||
|
|
||||||
def on_option_change(self, e: CheckboxChangeEvent, option: CateringModifierOption) -> None:
|
|
||||||
self.changed_options[option.key] = e.is_on
|
|
||||||
|
|
||||||
async def add_to_cart_pressed(self, _: PointerEvent) -> None:
|
|
||||||
if self.item.active:
|
|
||||||
await self.add_to_cart_pressed_callback(self.item, self.changed_options)
|
|
||||||
|
|
||||||
def build(self) -> Component:
|
|
||||||
base_mods = []
|
|
||||||
extra_mods = []
|
|
||||||
if self.item.active:
|
|
||||||
for modifier_group in self.item.modifier_groups:
|
|
||||||
if modifier_group.key == CateringModificationKey.BASE:
|
|
||||||
base_mods.append(Text("Basis:", fill=self.session.theme.secondary_color, font_size=0.8, margin_top=0.5))
|
|
||||||
container = FlowContainer(spacing=2.5)
|
|
||||||
for option in modifier_group.options:
|
|
||||||
container.children.append(Row(Checkbox(is_on=self.changed_options.get(option.key, option.default_selected), on_change=lambda event, option=option: self.on_option_change(event, option)), Text(option.label), spacing=0.6))
|
|
||||||
base_mods.append(container)
|
|
||||||
if modifier_group.key == CateringModificationKey.EXTRA:
|
|
||||||
extra_mods.append(Text("Extras:", fill=self.session.theme.secondary_color, font_size=0.8, margin_top=0.8))
|
|
||||||
container = FlowContainer(spacing=2.5)
|
|
||||||
for option in modifier_group.options:
|
|
||||||
text = f"{option.label}"
|
|
||||||
if option.price_delta > Decimal("0"):
|
|
||||||
text += f" (+ {self.make_money_string(option.price_delta)})"
|
|
||||||
container.children.append(Row(Checkbox(is_on=self.changed_options.get(option.key, option.default_selected), on_change=lambda event, option=option: self.on_option_change(event, option)), Text(text), spacing=0.6))
|
|
||||||
extra_mods.append(container)
|
|
||||||
|
|
||||||
return Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Row(
|
|
||||||
Column(
|
|
||||||
Row(
|
|
||||||
Text(text=self.item.name, overflow="nowrap", justify="left", font_size=1.1, margin_right=0.8, font_weight="bold", style=TextStyle(strikethrough=not self.item.active)),
|
|
||||||
Text(text=self.make_money_string(self.item.base_price), overflow="ellipsize", justify="left", font_size=0.8, grow_x=True, fill=self.session.theme.primary_color, align_y=1.2)
|
|
||||||
),
|
|
||||||
Text(self.item.description, font_size=0.7, margin_left=2),
|
|
||||||
*base_mods,
|
|
||||||
*extra_mods,
|
|
||||||
spacing=0.5,
|
|
||||||
margin=0.5,
|
|
||||||
grow_x=True
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
Spacer(),
|
|
||||||
PointerEventListener(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Icon(icon="material/add_shopping_cart", min_width=2, min_height=2, margin=1),
|
|
||||||
hover_fill=Color.TRANSPARENT if not self.item.active else self.session.theme.hud_color,
|
|
||||||
cursor="not-allowed" if not self.item.active else "pointer",
|
|
||||||
transition_time=0.2
|
|
||||||
),
|
|
||||||
on_press=self.add_to_cart_pressed
|
|
||||||
),
|
|
||||||
Spacer()
|
|
||||||
)
|
|
||||||
|
|
||||||
),
|
|
||||||
Separator(color=self.session.theme.box_border_color),
|
|
||||||
spacing=0.5,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@@ -17,11 +17,8 @@ class ElmButton(Component):
|
|||||||
style: Literal["small", "normal"] = "normal"
|
style: Literal["small", "normal"] = "normal"
|
||||||
wrap: bool = False
|
wrap: bool = False
|
||||||
is_loading: bool = False
|
is_loading: bool = False
|
||||||
is_disabled: bool = False
|
|
||||||
|
|
||||||
async def _on_press(self, event: PointerEvent) -> None:
|
async def _on_press(self, event: PointerEvent) -> None:
|
||||||
if self.is_disabled:
|
|
||||||
return
|
|
||||||
if iscoroutinefunction(self.on_press):
|
if iscoroutinefunction(self.on_press):
|
||||||
await self.on_press()
|
await self.on_press()
|
||||||
else:
|
else:
|
||||||
@@ -57,10 +54,10 @@ class ElmButton(Component):
|
|||||||
stroke_width=0.1,
|
stroke_width=0.1,
|
||||||
stroke_color=self.session.theme.secondary_color,
|
stroke_color=self.session.theme.secondary_color,
|
||||||
hover_stroke_width=0.1,
|
hover_stroke_width=0.1,
|
||||||
hover_stroke_color=self.session.theme.secondary_color if self.is_disabled else self.session.theme.hud_color,
|
hover_stroke_color=self.session.theme.hud_color,
|
||||||
hover_fill=Color.TRANSPARENT if self.is_disabled else self.session.theme.hud_color,
|
hover_fill=self.session.theme.hud_color,
|
||||||
transition_time=0,
|
transition_time=0,
|
||||||
cursor="not-allowed" if self.is_disabled else "pointer"
|
cursor="pointer"
|
||||||
),
|
),
|
||||||
on_press=self._on_press
|
on_press=self._on_press
|
||||||
)
|
)
|
||||||
@@ -1,23 +1,9 @@
|
|||||||
from rio import Component, Rectangle, Row, Text, Spacer, ProgressBar, Column, Color, TextStyle, Link
|
from rio import Component, Rectangle, Row, Text, Spacer, ProgressBar, Column, Color, TextStyle
|
||||||
from rio.event import on_populate
|
|
||||||
|
|
||||||
from elm.services import ConfigurationService
|
from elm.services import ConfigurationService
|
||||||
from elm.types import Ticket
|
|
||||||
|
|
||||||
|
|
||||||
class LanInfoBox(Component):
|
class LanInfoBox(Component):
|
||||||
total_tickets: int = 1
|
|
||||||
available_tickets: int = 0
|
|
||||||
|
|
||||||
@on_populate
|
|
||||||
async def on_populate(self) -> None:
|
|
||||||
total_tickets = 0
|
|
||||||
for ticket_info in self.session[ConfigurationService].get_ticket_info():
|
|
||||||
total_tickets += ticket_info.total_tickets
|
|
||||||
|
|
||||||
self.total_tickets = total_tickets
|
|
||||||
self.available_tickets = total_tickets - len(await Ticket.find_all().to_list())
|
|
||||||
|
|
||||||
def build(self) -> Component:
|
def build(self) -> Component:
|
||||||
lan_info = self.session[ConfigurationService].get_lan_info()
|
lan_info = self.session[ConfigurationService].get_lan_info()
|
||||||
if self.session.is_mobile():
|
if self.session.is_mobile():
|
||||||
@@ -36,7 +22,6 @@ class LanInfoBox(Component):
|
|||||||
stroke_color=self.session.theme.box_border_color,
|
stroke_color=self.session.theme.box_border_color,
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
Row(Text("Location:", font_size=0.7), Spacer(), Link(Text(lan_info.location_name, fill=self.session.theme.primary_color, font_size=0.8, overflow="nowrap"), target_url=lan_info.location_link, open_in_new_tab=True)),
|
|
||||||
Row(Text("Start:", font_size=0.7), Spacer(), Text(lan_info.date_from.strftime("%d.%m.%Y"), fill=self.session.theme.primary_color, font_size=0.8, overflow="nowrap")),
|
Row(Text("Start:", font_size=0.7), Spacer(), Text(lan_info.date_from.strftime("%d.%m.%Y"), fill=self.session.theme.primary_color, font_size=0.8, overflow="nowrap")),
|
||||||
Row(Text("Ende:", font_size=0.7), Spacer(), Text(lan_info.date_till.strftime("%d.%m.%Y"), fill=self.session.theme.primary_color, font_size=0.8, overflow="nowrap")),
|
Row(Text("Ende:", font_size=0.7), Spacer(), Text(lan_info.date_till.strftime("%d.%m.%Y"), fill=self.session.theme.primary_color, font_size=0.8, overflow="nowrap")),
|
||||||
Row(Text("Einlass:", font_size=0.7), Spacer(), Text(lan_info.date_from.strftime("%H:%M Uhr"), fill=self.session.theme.primary_color, font_size=0.8, overflow="nowrap")),
|
Row(Text("Einlass:", font_size=0.7), Spacer(), Text(lan_info.date_from.strftime("%H:%M Uhr"), fill=self.session.theme.primary_color, font_size=0.8, overflow="nowrap")),
|
||||||
@@ -47,10 +32,10 @@ class LanInfoBox(Component):
|
|||||||
Row(Text("Duschen:", font_size=0.7), Spacer(), Text("Ja" if lan_info.has_showers else "Nein", fill=self.session.theme.primary_color, font_size=0.8, overflow="nowrap")),
|
Row(Text("Duschen:", font_size=0.7), Spacer(), Text("Ja" if lan_info.has_showers else "Nein", fill=self.session.theme.primary_color, font_size=0.8, overflow="nowrap")),
|
||||||
Spacer(),
|
Spacer(),
|
||||||
Column(
|
Column(
|
||||||
Row(Text("Verfügbare Tickets", font_size=0.7, overflow="wrap"), Spacer(), Text(f"{self.available_tickets} / {self.total_tickets}", fill=self.session.theme.primary_color, font_size=0.8),
|
Row(Text("Verfügbare Tickets", font_size=0.7, overflow="wrap"), Spacer(), Text("22 / 60", fill=self.session.theme.primary_color, font_size=0.8),
|
||||||
margin_bottom=0.4),
|
margin_bottom=0.4),
|
||||||
ProgressBar(
|
ProgressBar(
|
||||||
progress=self.available_tickets / self.total_tickets,
|
progress=22 / 60,
|
||||||
margin=0.5,
|
margin=0.5,
|
||||||
rounded=False,
|
rounded=False,
|
||||||
min_height=0.4,
|
min_height=0.4,
|
||||||
@@ -82,7 +67,6 @@ class LanInfoBox(Component):
|
|||||||
stroke_color=self.session.theme.box_border_color,
|
stroke_color=self.session.theme.box_border_color,
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
Row(Text("Location:", font_size=0.9), Spacer(), Link(Text(lan_info.location_name, fill=self.session.theme.primary_color, font_size=0.9, overflow="nowrap"), target_url=lan_info.location_link, open_in_new_tab=True)),
|
|
||||||
Row(Text("Start:", font_size=0.9), Spacer(), Text(lan_info.date_from.strftime("%d.%m.%Y"), fill=self.session.theme.primary_color, font_size=0.9, overflow="nowrap")),
|
Row(Text("Start:", font_size=0.9), Spacer(), Text(lan_info.date_from.strftime("%d.%m.%Y"), fill=self.session.theme.primary_color, font_size=0.9, overflow="nowrap")),
|
||||||
Row(Text("Ende:", font_size=0.9), Spacer(), Text(lan_info.date_till.strftime("%d.%m.%Y"), fill=self.session.theme.primary_color, font_size=0.9, overflow="nowrap")),
|
Row(Text("Ende:", font_size=0.9), Spacer(), Text(lan_info.date_till.strftime("%d.%m.%Y"), fill=self.session.theme.primary_color, font_size=0.9, overflow="nowrap")),
|
||||||
Row(Text("Einlass:", font_size=0.9), Spacer(), Text(lan_info.date_from.strftime("%H:%M Uhr"), fill=self.session.theme.primary_color, font_size=0.9, overflow="nowrap")),
|
Row(Text("Einlass:", font_size=0.9), Spacer(), Text(lan_info.date_from.strftime("%H:%M Uhr"), fill=self.session.theme.primary_color, font_size=0.9, overflow="nowrap")),
|
||||||
@@ -93,9 +77,9 @@ class LanInfoBox(Component):
|
|||||||
Row(Text("Duschen:", font_size=0.9), Spacer(), Text("Ja" if lan_info.has_showers else "Nein", fill=self.session.theme.primary_color, font_size=0.9, overflow="nowrap")),
|
Row(Text("Duschen:", font_size=0.9), Spacer(), Text("Ja" if lan_info.has_showers else "Nein", fill=self.session.theme.primary_color, font_size=0.9, overflow="nowrap")),
|
||||||
Spacer(),
|
Spacer(),
|
||||||
Column(
|
Column(
|
||||||
Row(Text("Verfügbare Tickets", font_size=0.9, overflow="wrap"), Spacer(), Text(f"{self.available_tickets} / {self.total_tickets}", fill=self.session.theme.primary_color, font_size=0.9), margin_bottom=0.4),
|
Row(Text("Verfügbare Tickets", font_size=0.9, overflow="wrap"), Spacer(), Text("22 / 60", fill=self.session.theme.primary_color, font_size=0.9), margin_bottom=0.4),
|
||||||
ProgressBar(
|
ProgressBar(
|
||||||
progress=self.available_tickets / self.total_tickets,
|
progress=22 / 60,
|
||||||
margin=0.5,
|
margin=0.5,
|
||||||
rounded=False,
|
rounded=False,
|
||||||
min_height=0.4,
|
min_height=0.4,
|
||||||
|
|||||||
@@ -47,18 +47,14 @@ class LandingPageBoxHalf(Component):
|
|||||||
stroke_color=self.session.theme.box_border_color,
|
stroke_color=self.session.theme.box_border_color,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
Column(
|
Rectangle(
|
||||||
Rectangle(
|
content=Image(
|
||||||
content=Image(
|
from_root(f"src/elm/assets/img/{self.image_name}")
|
||||||
from_root(f"src/elm/assets/img/{self.image_name}")
|
|
||||||
),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
margin_right=1,
|
|
||||||
min_width=4 if self.session.is_mobile() else 10,
|
|
||||||
min_height=4 if self.session.is_mobile() else 10,
|
|
||||||
),
|
),
|
||||||
Spacer()
|
stroke_width=0.1,
|
||||||
|
stroke_color=self.session.theme.box_border_color,
|
||||||
|
margin_right=1,
|
||||||
|
min_width=4 if self.session.is_mobile() else 10
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
self.article_text,
|
self.article_text,
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ class NavigationButton(Component):
|
|||||||
else:
|
else:
|
||||||
self.session.navigate_to(self.target_url)
|
self.session.navigate_to(self.target_url)
|
||||||
|
|
||||||
if self.extension_state_changed is not None:
|
await self.extension_state_changed(None)
|
||||||
await self.extension_state_changed(None)
|
|
||||||
|
|
||||||
|
|
||||||
def is_active(self) -> bool:
|
def is_active(self) -> bool:
|
||||||
@@ -94,14 +93,13 @@ class NavigationBar(Component):
|
|||||||
NavigationButton("material/house", "Startseite", "/", extension_state_changed=self.on_extension_pressed),
|
NavigationButton("material/house", "Startseite", "/", extension_state_changed=self.on_extension_pressed),
|
||||||
NavigationButton("material/local_activity", "Tickets", "/tickets", extension_state_changed=self.on_extension_pressed),
|
NavigationButton("material/local_activity", "Tickets", "/tickets", extension_state_changed=self.on_extension_pressed),
|
||||||
NavigationButton("material/chair_alt", "Sitzplan", "/seating", extension_state_changed=self.on_extension_pressed),
|
NavigationButton("material/chair_alt", "Sitzplan", "/seating", extension_state_changed=self.on_extension_pressed),
|
||||||
NavigationButton("material/group", "Teilnehmer", "/participants", extension_state_changed=self.on_extension_pressed),
|
|
||||||
NavigationButton("material/local_dining", "Catering", "/catering", extension_state_changed=self.on_extension_pressed),
|
NavigationButton("material/local_dining", "Catering", "/catering", extension_state_changed=self.on_extension_pressed),
|
||||||
NavigationButton("material/trophy", "Turniere", "/tournaments", extension_state_changed=self.on_extension_pressed),
|
NavigationButton("material/trophy", "Turniere", "/tournaments", extension_state_changed=self.on_extension_pressed),
|
||||||
margin_bottom=6
|
margin_bottom=6
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
NavigationButton("material/help", "FAQ", "/faq", extension_state_changed=self.on_extension_pressed),
|
NavigationButton("material/help", "FAQ", "/faq", extension_state_changed=self.on_extension_pressed),
|
||||||
NavigationButton("material/contact_page", "Kontakt", "mailto:tech@ezgg-ev.de?subject=Kontaktanfrage", new_tab=True, extension_state_changed=self.on_extension_pressed),
|
NavigationButton("material/contact_page", "Kontakt", "/contact", extension_state_changed=self.on_extension_pressed),
|
||||||
NavigationButton("material/sports_bar", "EZ GG e.V.", "https://ezgg-ev.de", new_tab=True, extension_state_changed=None),
|
NavigationButton("material/sports_bar", "EZ GG e.V.", "https://ezgg-ev.de", new_tab=True, extension_state_changed=None),
|
||||||
NavigationButton("material/article", "Impressum", "/imprint", extension_state_changed=self.on_extension_pressed),
|
NavigationButton("material/article", "Impressum", "/imprint", extension_state_changed=self.on_extension_pressed),
|
||||||
NavigationButton("material/balance", "Regeln & AGB", "/rules", extension_state_changed=self.on_extension_pressed),
|
NavigationButton("material/balance", "Regeln & AGB", "/rules", extension_state_changed=self.on_extension_pressed),
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from rio import Component, Rectangle, Grid, Column, Row, Text, TextStyle, Color, Spacer
|
|
||||||
|
|
||||||
from elm.components import SeatPixel, WallPixel, TextPixel
|
|
||||||
from elm.types import Seat
|
|
||||||
|
|
||||||
MAX_GRID_WIDTH_PIXELS = 70
|
|
||||||
MAX_GRID_HEIGHT_PIXELS = 60
|
|
||||||
|
|
||||||
class SeatingPlan(Component):
|
|
||||||
preloaded_seats: list[Seat]
|
|
||||||
|
|
||||||
def get_seat(self, seat_id: str) -> Optional[Seat]:
|
|
||||||
return next(filter(lambda x: x.seat_id == seat_id, self.preloaded_seats), None)
|
|
||||||
|
|
||||||
"""
|
|
||||||
This seating plan is for the community center "Donsbach"
|
|
||||||
"""
|
|
||||||
def build(self) -> Component:
|
|
||||||
grid = Grid()
|
|
||||||
# Outlines
|
|
||||||
for x in range(0, MAX_GRID_WIDTH_PIXELS):
|
|
||||||
grid.add(WallPixel(), row=0, column=x)
|
|
||||||
|
|
||||||
for y in range(0, MAX_GRID_HEIGHT_PIXELS):
|
|
||||||
grid.add(WallPixel(), row=y, column=0)
|
|
||||||
|
|
||||||
for x in range(0, MAX_GRID_WIDTH_PIXELS):
|
|
||||||
grid.add(WallPixel(), row=MAX_GRID_HEIGHT_PIXELS, column=x)
|
|
||||||
|
|
||||||
for x in range(0, 41):
|
|
||||||
grid.add(WallPixel(), row=15, column=x)
|
|
||||||
|
|
||||||
for x in range(51, MAX_GRID_WIDTH_PIXELS):
|
|
||||||
grid.add(WallPixel(), row=32, column=x)
|
|
||||||
|
|
||||||
for x in range(41, 44):
|
|
||||||
grid.add(WallPixel(), row=32, column=x)
|
|
||||||
grid.add(WallPixel(), row=19, column=x)
|
|
||||||
|
|
||||||
for x in range(52, MAX_GRID_WIDTH_PIXELS):
|
|
||||||
grid.add(WallPixel(), row=11, column=x)
|
|
||||||
grid.add(WallPixel(), row=22, column=x)
|
|
||||||
|
|
||||||
for x in range(32, 40):
|
|
||||||
grid.add(WallPixel(), row=5, column=x)
|
|
||||||
grid.add(WallPixel(), row=10, column=x)
|
|
||||||
|
|
||||||
|
|
||||||
for y in range(5, 11):
|
|
||||||
grid.add(WallPixel(), row=y, column=31)
|
|
||||||
grid.add(WallPixel(), row=y, column=40)
|
|
||||||
|
|
||||||
for y in range(40, MAX_GRID_HEIGHT_PIXELS):
|
|
||||||
grid.add(WallPixel(), row=y, column=40)
|
|
||||||
|
|
||||||
for y in range(32, 36):
|
|
||||||
grid.add(WallPixel(), row=y, column=40)
|
|
||||||
|
|
||||||
for y in range(19, 33):
|
|
||||||
grid.add(WallPixel(), row=y, column=44)
|
|
||||||
|
|
||||||
for y in range(16, 20):
|
|
||||||
grid.add(WallPixel(), row=y, column=40)
|
|
||||||
|
|
||||||
for y in range(0, 5):
|
|
||||||
grid.add(WallPixel(), row=y, column=51)
|
|
||||||
|
|
||||||
for y in range(9, 15):
|
|
||||||
grid.add(WallPixel(), row=y, column=51)
|
|
||||||
|
|
||||||
for y in range(19, 33):
|
|
||||||
grid.add(WallPixel(), row=y, column=51)
|
|
||||||
|
|
||||||
|
|
||||||
# Block A
|
|
||||||
grid.add(SeatPixel("A01", seat=self.get_seat("A01"), seat_orientation="bottom"), row=57, column=1, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("A02", seat=self.get_seat("A02"), seat_orientation="bottom"), row=57, column=7, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("A03", seat=self.get_seat("A03"), seat_orientation="bottom"), row=57, column=13, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("A04", seat=self.get_seat("A04"), seat_orientation="bottom"), row=57, column=19, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("A05", seat=self.get_seat("A05"), seat_orientation="bottom"), row=57, column=25, width=6, height=2)
|
|
||||||
|
|
||||||
grid.add(SeatPixel("A10", seat=self.get_seat("A10"), seat_orientation="top"), row=55, column=1, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("A11", seat=self.get_seat("A11"), seat_orientation="top"), row=55, column=7, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("A12", seat=self.get_seat("A12"), seat_orientation="top"), row=55, column=13, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("A13", seat=self.get_seat("A13"), seat_orientation="top"), row=55, column=19, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("A14", seat=self.get_seat("A14"), seat_orientation="top"), row=55, column=25, width=6, height=2)
|
|
||||||
|
|
||||||
# Block B
|
|
||||||
grid.add(SeatPixel("B01", seat=self.get_seat("B01"), seat_orientation="bottom"), row=50, column=1, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("B02", seat=self.get_seat("B02"), seat_orientation="bottom"), row=50, column=4, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("B03", seat=self.get_seat("B03"), seat_orientation="bottom"), row=50, column=7, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("B04", seat=self.get_seat("B04"), seat_orientation="bottom"), row=50, column=10, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("B05", seat=self.get_seat("B05"), seat_orientation="bottom"), row=50, column=13, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("B06", seat=self.get_seat("B06"), seat_orientation="bottom"), row=50, column=16, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("B07", seat=self.get_seat("B07"), seat_orientation="bottom"), row=50, column=19, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("B08", seat=self.get_seat("B08"), seat_orientation="bottom"), row=50, column=22, width=3, height=2)
|
|
||||||
|
|
||||||
grid.add(SeatPixel("B10", seat=self.get_seat("B10"), seat_orientation="top"), row=48, column=1, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("B11", seat=self.get_seat("B11"), seat_orientation="top"), row=48, column=4, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("B12", seat=self.get_seat("B12"), seat_orientation="top"), row=48, column=7, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("B13", seat=self.get_seat("B13"), seat_orientation="top"), row=48, column=10, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("B14", seat=self.get_seat("B14"), seat_orientation="top"), row=48, column=13, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("B15", seat=self.get_seat("B15"), seat_orientation="top"), row=48, column=16, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("B16", seat=self.get_seat("B16"), seat_orientation="top"), row=48, column=19, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("B17", seat=self.get_seat("B17"), seat_orientation="top"), row=48, column=22, width=3, height=2)
|
|
||||||
|
|
||||||
# Block C
|
|
||||||
grid.add(SeatPixel("C01", seat=self.get_seat("C01"), seat_orientation="bottom"), row=43, column=1, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("C02", seat=self.get_seat("C02"), seat_orientation="bottom"), row=43, column=4, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("C03", seat=self.get_seat("C03"), seat_orientation="bottom"), row=43, column=7, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("C04", seat=self.get_seat("C04"), seat_orientation="bottom"), row=43, column=10, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("C05", seat=self.get_seat("C05"), seat_orientation="bottom"), row=43, column=13, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("C06", seat=self.get_seat("C06"), seat_orientation="bottom"), row=43, column=16, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("C07", seat=self.get_seat("C07"), seat_orientation="bottom"), row=43, column=19, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("C08", seat=self.get_seat("C08"), seat_orientation="bottom"), row=43, column=22, width=3, height=2)
|
|
||||||
|
|
||||||
|
|
||||||
grid.add(SeatPixel("C10", seat=self.get_seat("C10"), seat_orientation="top"), row=41, column=1, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("C11", seat=self.get_seat("C11"), seat_orientation="top"), row=41, column=4, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("C12", seat=self.get_seat("C12"), seat_orientation="top"), row=41, column=7, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("C13", seat=self.get_seat("C13"), seat_orientation="top"), row=41, column=10, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("C14", seat=self.get_seat("C14"), seat_orientation="top"), row=41, column=13, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("C15", seat=self.get_seat("C15"), seat_orientation="top"), row=41, column=16, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("C16", seat=self.get_seat("C16"), seat_orientation="top"), row=41, column=19, width=3, height=2)
|
|
||||||
grid.add(SeatPixel("C17", seat=self.get_seat("C17"), seat_orientation="top"), row=41, column=22, width=3, height=2)
|
|
||||||
|
|
||||||
# Block D
|
|
||||||
grid.add(SeatPixel("D01", seat=self.get_seat("D01"), seat_orientation="bottom"), row=34, column=1, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("D02", seat=self.get_seat("D02"), seat_orientation="bottom"), row=34, column=7, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("D03", seat=self.get_seat("D03"), seat_orientation="bottom"), row=34, column=13, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("D04", seat=self.get_seat("D04"), seat_orientation="bottom"), row=34, column=19, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("D05", seat=self.get_seat("D05"), seat_orientation="bottom"), row=34, column=25, width=6, height=2)
|
|
||||||
|
|
||||||
grid.add(SeatPixel("D10", seat=self.get_seat("D10"), seat_orientation="top"), row=32, column=1, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("D11", seat=self.get_seat("D11"), seat_orientation="top"), row=32, column=7, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("D12", seat=self.get_seat("D12"), seat_orientation="top"), row=32, column=13, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("D13", seat=self.get_seat("D13"), seat_orientation="top"), row=32, column=19, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("D14", seat=self.get_seat("D14"), seat_orientation="top"), row=32, column=25, width=6, height=2)
|
|
||||||
|
|
||||||
# Block E
|
|
||||||
grid.add(SeatPixel("E01", seat=self.get_seat("E01"), seat_orientation="bottom"), row=27, column=1, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("E02", seat=self.get_seat("E02"), seat_orientation="bottom"), row=27, column=7, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("E03", seat=self.get_seat("E03"), seat_orientation="bottom"), row=27, column=13, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("E04", seat=self.get_seat("E04"), seat_orientation="bottom"), row=27, column=19, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("E05", seat=self.get_seat("E05"), seat_orientation="bottom"), row=27, column=25, width=6, height=2)
|
|
||||||
|
|
||||||
grid.add(SeatPixel("E10", seat=self.get_seat("E10"), seat_orientation="top"), row=25, column=1, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("E11", seat=self.get_seat("E11"), seat_orientation="top"), row=25, column=7, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("E12", seat=self.get_seat("E12"), seat_orientation="top"), row=25, column=13, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("E13", seat=self.get_seat("E13"), seat_orientation="top"), row=25, column=19, width=6, height=2)
|
|
||||||
grid.add(SeatPixel("E14", seat=self.get_seat("E14"), seat_orientation="top"), row=25, column=25, width=6, height=2)
|
|
||||||
|
|
||||||
# Orga Block
|
|
||||||
grid.add(SeatPixel("Z01", seat=self.get_seat("Z01"), seat_orientation="top"), row=40, column=35, width=4, height=2)
|
|
||||||
grid.add(SeatPixel("Z02", seat=self.get_seat("Z02"), seat_orientation="top"), row=40, column=31, width=4, height=2)
|
|
||||||
grid.add(SeatPixel("Z\n0\n3", seat=self.get_seat("Z03"), seat_orientation="top"), row=40, column=29, width=2, height=6)
|
|
||||||
grid.add(SeatPixel("Z\n0\n4", seat=self.get_seat("Z04"), seat_orientation="bottom"), row=46, column=29, width=2, height=6)
|
|
||||||
grid.add(SeatPixel("Z05", seat=self.get_seat("Z05"), seat_orientation="bottom"), row=50, column=31, width=4, height=2)
|
|
||||||
grid.add(SeatPixel("Z06", seat=self.get_seat("Z06"), seat_orientation="bottom"), row=50, column=35, width=5, height=2)
|
|
||||||
|
|
||||||
# Stage
|
|
||||||
grid.add(TextPixel(text="Bühne"), row=16, column=1, width=39, height=4)
|
|
||||||
|
|
||||||
# Drinks
|
|
||||||
grid.add(TextPixel(text="G\ne\nt\nr\nä\nn\nk\ne"), row=20, column=40, width=4, height=12)
|
|
||||||
|
|
||||||
# Main Entrance
|
|
||||||
grid.add(TextPixel(text="H\na\nl\nl\ne\nn\n\nE\ni\nn\ng\na\nn\ng"), row=33, column=66, width=4, height=27)
|
|
||||||
|
|
||||||
# Sleeping
|
|
||||||
grid.add(TextPixel(icon_name="material/bed"), row=1, column=1, width=30, height=14)
|
|
||||||
|
|
||||||
# Toilet
|
|
||||||
grid.add(TextPixel(icon_name="material/wc"), row=1, column=52, width=19, height=10)
|
|
||||||
grid.add(TextPixel(icon_name="material/wc"), row=12, column=52, width=19, height=10)
|
|
||||||
|
|
||||||
return grid
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
from rio import Component, Text, Icon, TextStyle, Rectangle, Spacer, Color, PointerEventListener, Row, PointerEvent, Tooltip
|
|
||||||
from typing import Optional, Literal
|
|
||||||
|
|
||||||
from rio.event import on_populate
|
|
||||||
|
|
||||||
from elm.types import Seat, UserSession, User
|
|
||||||
|
|
||||||
|
|
||||||
class SeatPixel(Component):
|
|
||||||
seat_id: str
|
|
||||||
seat_orientation: Literal["top", "bottom"]
|
|
||||||
seat: Optional[Seat] = None
|
|
||||||
associated_user: Optional[User] = None
|
|
||||||
|
|
||||||
@on_populate
|
|
||||||
async def on_populate(self) -> None:
|
|
||||||
if self.seat is None:
|
|
||||||
self.seat = await Seat.find_one(Seat.seat_id == self.seat_id.replace("\n", ""), fetch_links=True)
|
|
||||||
if self.seat and self.seat.user is not None:
|
|
||||||
self.associated_user = await self.seat.user.fetch()
|
|
||||||
|
|
||||||
async def on_press(self, _: PointerEvent) -> None:
|
|
||||||
seat_id = self.seat_id.replace("\n", "")
|
|
||||||
self.session.navigate_to(f"./seat-info?seat_id={seat_id}")
|
|
||||||
|
|
||||||
def determine_color(self) -> Color:
|
|
||||||
if self.seat is not None:
|
|
||||||
try:
|
|
||||||
user_name = self.session[UserSession].user_name
|
|
||||||
except KeyError:
|
|
||||||
user_name = None
|
|
||||||
|
|
||||||
if self.seat.user is not None and self.associated_user and self.associated_user.user_name == user_name:
|
|
||||||
return Color.from_hex("800080")
|
|
||||||
elif self.seat.is_blocked or self.seat.user is not None:
|
|
||||||
return self.session.theme.danger_color
|
|
||||||
return self.session.theme.success_color
|
|
||||||
return self.session.theme.success_color
|
|
||||||
|
|
||||||
def build(self) -> Component:
|
|
||||||
if self.seat is None:
|
|
||||||
return Spacer()
|
|
||||||
text = Text(f"{self.seat_id}", style=TextStyle(fill=Color.BLACK, font_size=0.9), align_x=0.5, selectable=False)
|
|
||||||
rec = Rectangle(
|
|
||||||
content=Row(text),
|
|
||||||
min_width=1,
|
|
||||||
min_height=1,
|
|
||||||
fill=self.determine_color(),
|
|
||||||
stroke_width=0.1,
|
|
||||||
hover_stroke_width=0.1,
|
|
||||||
stroke_color=Color.BLACK,
|
|
||||||
grow_x=True,
|
|
||||||
grow_y=True,
|
|
||||||
hover_fill=self.session.theme.hud_color,
|
|
||||||
transition_time=0.4,
|
|
||||||
ripple=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.associated_user or self.seat.is_blocked:
|
|
||||||
return PointerEventListener(
|
|
||||||
content=Tooltip(
|
|
||||||
anchor=rec,
|
|
||||||
tip=self.associated_user.user_name if self.associated_user else "Gesperrt",
|
|
||||||
position=self.seat_orientation,
|
|
||||||
),
|
|
||||||
on_press=self.on_press,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return PointerEventListener(
|
|
||||||
content=rec,
|
|
||||||
on_press=self.on_press,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TextPixel(Component):
|
|
||||||
text: Optional[str] = None
|
|
||||||
icon_name: Optional[str] = None
|
|
||||||
no_outline: bool = False
|
|
||||||
|
|
||||||
def build(self) -> Component:
|
|
||||||
if self.text is not None:
|
|
||||||
content = Text(self.text, style=TextStyle(fill=self.session.theme.text_color, font_size=1), align_x=0.5, selectable=False)
|
|
||||||
elif self.icon_name is not None:
|
|
||||||
content = Icon(self.icon_name, fill=self.session.theme.text_color)
|
|
||||||
else:
|
|
||||||
content = None
|
|
||||||
return Rectangle(
|
|
||||||
content=content,
|
|
||||||
min_width=1,
|
|
||||||
min_height=1,
|
|
||||||
fill=self.session.theme.background_color,
|
|
||||||
stroke_width=0.0 if self.no_outline else 0.1,
|
|
||||||
stroke_color=self.session.theme.neutral_color,
|
|
||||||
hover_stroke_width=None if self.no_outline else 0.1,
|
|
||||||
grow_x=True,
|
|
||||||
grow_y=True,
|
|
||||||
hover_fill=None,
|
|
||||||
ripple=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class WallPixel(Component):
|
|
||||||
def build(self) -> Component:
|
|
||||||
return Rectangle(
|
|
||||||
min_width=1,
|
|
||||||
min_height=1,
|
|
||||||
fill=Color.from_hex("434343"),
|
|
||||||
grow_x=True,
|
|
||||||
grow_y=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DebugPixel(Component):
|
|
||||||
def build(self) -> Component:
|
|
||||||
return Rectangle(
|
|
||||||
content=Spacer(),
|
|
||||||
min_width=1,
|
|
||||||
min_height=1,
|
|
||||||
fill=self.session.theme.success_color,
|
|
||||||
hover_stroke_color=self.session.theme.hud_color,
|
|
||||||
hover_stroke_width=0.1,
|
|
||||||
grow_x=True,
|
|
||||||
grow_y=True,
|
|
||||||
hover_fill=self.session.theme.secondary_color,
|
|
||||||
transition_time=0.1
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class InvisiblePixel(Component):
|
|
||||||
def build(self) -> Component:
|
|
||||||
return Rectangle(
|
|
||||||
content=Spacer(),
|
|
||||||
min_width=1,
|
|
||||||
min_height=1,
|
|
||||||
fill=Color.TRANSPARENT,
|
|
||||||
hover_stroke_width=0.0,
|
|
||||||
grow_x=True,
|
|
||||||
grow_y=True
|
|
||||||
)
|
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
from asyncio import sleep
|
from typing import Optional, Callable
|
||||||
from decimal import Decimal
|
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
from rio import Component, Row, Column, Color, PointerEventListener, PointerEvent, Rectangle, Text, TextStyle, event
|
from rio import Component, Row, Column, Color, PointerEventListener, PointerEvent, Rectangle, Text, TextStyle, Icon, event
|
||||||
|
|
||||||
from elm.types import UserSession
|
|
||||||
from elm.services import AccountingService
|
|
||||||
|
|
||||||
|
|
||||||
class UserNavigationButton(Component):
|
class UserNavigationButton(Component):
|
||||||
@@ -66,54 +61,19 @@ class UserNavigationButton(Component):
|
|||||||
|
|
||||||
class UserNavigation(Component):
|
class UserNavigation(Component):
|
||||||
close_navigation: Callable
|
close_navigation: Callable
|
||||||
balance: Decimal = Decimal(0)
|
|
||||||
|
|
||||||
@event.on_page_change
|
@event.on_page_change
|
||||||
async def on_page_change(self) -> None:
|
async def on_page_change(self) -> None:
|
||||||
await self.close_navigation()
|
await self.close_navigation()
|
||||||
|
|
||||||
async def update_balance(self) -> None:
|
|
||||||
try:
|
|
||||||
balance = await self.session[AccountingService].get_balance(self.session[UserSession].user_name)
|
|
||||||
if balance != self.balance:
|
|
||||||
self.balance = balance
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
await sleep(5)
|
|
||||||
self.session.create_task(self.update_balance())
|
|
||||||
|
|
||||||
@event.on_populate
|
|
||||||
async def on_populate(self) -> None:
|
|
||||||
self.session.create_task(self.update_balance())
|
|
||||||
|
|
||||||
def show_admin_navigation(self) -> bool:
|
|
||||||
try:
|
|
||||||
user_session = self.session[UserSession]
|
|
||||||
except KeyError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return user_session.is_team_member
|
|
||||||
|
|
||||||
def build(self) -> Component:
|
def build(self) -> Component:
|
||||||
base_nav = [
|
|
||||||
UserNavigationButton(f"Guthaben: {self.session[AccountingService].make_euro_string_from_decimal(self.balance)}", "/balance", self.close_navigation),
|
|
||||||
UserNavigationButton("Meine Bestellungen", "/my-orders", self.close_navigation),
|
|
||||||
UserNavigationButton("Mein Profil", "/my-profile", self.close_navigation)
|
|
||||||
]
|
|
||||||
|
|
||||||
if self.show_admin_navigation():
|
|
||||||
base_nav.extend([
|
|
||||||
UserNavigationButton("Admin: Benutzer", "/user-admin", self.close_navigation),
|
|
||||||
UserNavigationButton("Admin: Catering", "/catering-admin", self.close_navigation)
|
|
||||||
])
|
|
||||||
|
|
||||||
base_nav.append(
|
|
||||||
UserNavigationButton("Ausloggen", "/logout", self.close_navigation)
|
|
||||||
)
|
|
||||||
|
|
||||||
return Rectangle(
|
return Rectangle(
|
||||||
content=Column(*base_nav),
|
content=Column(
|
||||||
|
UserNavigationButton("Guthaben: 0,00 €", "/balance", self.close_navigation),
|
||||||
|
UserNavigationButton("Mein Profil", "/my-profile", self.close_navigation),
|
||||||
|
UserNavigationButton("Mein Clan", "/my-clans", self.close_navigation),
|
||||||
|
UserNavigationButton("Ausloggen", "/logout", self.close_navigation)
|
||||||
|
),
|
||||||
min_width=3.5,
|
min_width=3.5,
|
||||||
min_height=3.5,
|
min_height=3.5,
|
||||||
fill=self.session.theme.background_color
|
fill=self.session.theme.background_color
|
||||||
|
|||||||
@@ -6,9 +6,3 @@ from .ElmButton import ElmButton
|
|||||||
from .AvatarEditBox import AvatarEditBox
|
from .AvatarEditBox import AvatarEditBox
|
||||||
from .AccountInfoBox import AccountInfoBox
|
from .AccountInfoBox import AccountInfoBox
|
||||||
from .PersonalInfoBox import PersonalInfoBox
|
from .PersonalInfoBox import PersonalInfoBox
|
||||||
from .BuyTicketBox import BuyTicketBox
|
|
||||||
from .SeatingPlanPixels import *
|
|
||||||
from .SeatingPlan import *
|
|
||||||
from .CateringItemBox import CateringItemBox
|
|
||||||
from .CateringCategoryDisplay import CateringCategoryDisplay
|
|
||||||
from .CateringCart import CateringCart
|
|
||||||
|
|||||||
@@ -1,224 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from asyncio import sleep
|
|
||||||
from functools import partial
|
|
||||||
from typing import Optional
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from beanie import PydanticObjectId
|
|
||||||
from rio import Component, Column, Row, Text, Spacer, page, Rectangle, GuardEvent, FlowContainer, List, PointerEventListener, Overlay, Link, Switch, SwitchChangeEvent
|
|
||||||
from rio.event import on_populate
|
|
||||||
|
|
||||||
from elm.types import UserSession, User, Seat
|
|
||||||
from elm.services import AccountingService, ReceiptPrintingService
|
|
||||||
from elm.components import ElmButton
|
|
||||||
from elm.types.CateringTypes import CateringOrder, CateringOrderStatus, CateringMenuItem, CateringMenuItemCategory
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
|
||||||
|
|
||||||
def catering_admin_page_guard(event: GuardEvent) -> Optional[str]:
|
|
||||||
try:
|
|
||||||
if event.session[UserSession].is_team_member:
|
|
||||||
return None
|
|
||||||
return "/"
|
|
||||||
except KeyError:
|
|
||||||
return "/"
|
|
||||||
|
|
||||||
@page(name="Cateringverwaltung", url_segment="catering-admin", guard=catering_admin_page_guard)
|
|
||||||
class CateringAdminPage(Component):
|
|
||||||
open_orders: List[CateringOrder] = List()
|
|
||||||
all_users: list[User] = []
|
|
||||||
all_seats: list[Seat] = []
|
|
||||||
all_menu_items: list[CateringMenuItem] = []
|
|
||||||
edited_order: Optional[CateringOrder] = None
|
|
||||||
|
|
||||||
@on_populate
|
|
||||||
async def on_populate(self) -> None:
|
|
||||||
self.all_users = await User.find_all().to_list()
|
|
||||||
self.all_seats = await Seat.find_all(fetch_links=True).to_list()
|
|
||||||
self.all_menu_items = await CateringMenuItem.find_all(fetch_links=True).to_list()
|
|
||||||
self.open_orders = List(await CateringOrder.find_many(
|
|
||||||
{
|
|
||||||
"status": {
|
|
||||||
"$nin": [
|
|
||||||
CateringOrderStatus.COMPLETED,
|
|
||||||
CateringOrderStatus.CANCELED,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).to_list())
|
|
||||||
await sleep(5)
|
|
||||||
self.session.create_task(self.on_populate())
|
|
||||||
|
|
||||||
def get_name_for_user_id(self, id_: PydanticObjectId) -> str:
|
|
||||||
return next(filter(lambda user: user.id == id_ ,self.all_users)).user_name
|
|
||||||
|
|
||||||
def get_seat_for_user_id(self, id_: PydanticObjectId) -> str:
|
|
||||||
try:
|
|
||||||
found_seat: Optional[Seat] = next(filter(lambda seat: seat.user is not None and seat.user.id == id_, self.all_seats), None)
|
|
||||||
if found_seat:
|
|
||||||
return found_seat.seat_id
|
|
||||||
return "-"
|
|
||||||
except Exception:
|
|
||||||
return "-"
|
|
||||||
|
|
||||||
async def on_order_pressed(self, order: CateringOrder) -> None:
|
|
||||||
self.edited_order = order
|
|
||||||
|
|
||||||
async def change_order_status(self, new_status: CateringOrderStatus) -> None:
|
|
||||||
if not self.edited_order:
|
|
||||||
return
|
|
||||||
|
|
||||||
if new_status == CateringOrderStatus.CANCELED:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if self.edited_order.status == new_status:
|
|
||||||
self.edited_order = None
|
|
||||||
return
|
|
||||||
|
|
||||||
if new_status == CateringOrderStatus.CANCELED:
|
|
||||||
user = await User.find_one(User.id == self.edited_order.customer_id)
|
|
||||||
if not user:
|
|
||||||
self.edited_order = None
|
|
||||||
return
|
|
||||||
|
|
||||||
price = Decimal(0)
|
|
||||||
for item in self.edited_order.items:
|
|
||||||
price += item.final_unit_price
|
|
||||||
await self.session[AccountingService].add_balance(user.user_name, price, f"CATERING REFUND - {str(self.edited_order.id)[-5:]}")
|
|
||||||
|
|
||||||
self.edited_order.status = new_status
|
|
||||||
await self.edited_order.save()
|
|
||||||
self.open_orders = List(await CateringOrder.find_many(
|
|
||||||
{
|
|
||||||
"status": {
|
|
||||||
"$nin": [
|
|
||||||
CateringOrderStatus.COMPLETED,
|
|
||||||
CateringOrderStatus.CANCELED,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).to_list())
|
|
||||||
self.edited_order = None
|
|
||||||
|
|
||||||
async def print_receipt(self) -> None:
|
|
||||||
if not self.edited_order:
|
|
||||||
return
|
|
||||||
user = await User.find_one(User.id == self.edited_order.customer_id)
|
|
||||||
if not user:
|
|
||||||
self.edited_order = None
|
|
||||||
return
|
|
||||||
self.session.create_task(self.session[ReceiptPrintingService].print_order(user, self.edited_order))
|
|
||||||
self.edited_order = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def change_item_active(event: SwitchChangeEvent, item: CateringMenuItem) -> None:
|
|
||||||
item.active = event.is_on
|
|
||||||
await item.save()
|
|
||||||
|
|
||||||
def build(self) -> Component:
|
|
||||||
if self.edited_order:
|
|
||||||
overlay = [
|
|
||||||
Overlay(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Text(f"Status ändern - Bestellung {str(self.edited_order.id)[-5:]}", margin_bottom=1),
|
|
||||||
*[ElmButton(text=CateringOrder.translate_order_status(status), on_press=partial(self.change_order_status, status)) for status in CateringOrderStatus],
|
|
||||||
Row(ElmButton(text="Bon drucken", on_press=self.print_receipt), ElmButton(text="Abbrechen", on_press=lambda: self.__setattr__("edited_order", None)), spacing=1, margin_top=2),
|
|
||||||
spacing=0.5,
|
|
||||||
margin=1
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.2,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
align_x=0.5,
|
|
||||||
align_y=0.5
|
|
||||||
)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
overlay = []
|
|
||||||
return Row(
|
|
||||||
*overlay,
|
|
||||||
Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Row(
|
|
||||||
Text("Offene Bestellungen", margin=0.5, selectable=False, overflow="wrap"),
|
|
||||||
Link(content="Neue Bestellung", target_url="./new-pos-order")
|
|
||||||
),
|
|
||||||
fill=self.session.theme.header_box_background_color,
|
|
||||||
margin=0.4
|
|
||||||
),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
),
|
|
||||||
FlowContainer(
|
|
||||||
*[PointerEventListener(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Row(Text(f"ID:", font_size=1.2), Text(str(order.id)[-5:], justify="right", font_size=1.2)),
|
|
||||||
Row(Text("Nutzer:", font_size=1.2), Text(self.get_name_for_user_id(order.customer_id), font_size=1.2, justify="right")),
|
|
||||||
Row(Text(f"Sitzplatz:", font_size=1.2), Text(self.get_seat_for_user_id(order.customer_id), font_size=1.2, justify="right")),
|
|
||||||
Row(Text(f"Status:", font_size=1.2), Text(CateringOrder.translate_order_status(order.status), font_size=1.2, justify="right"), margin_bottom=2),
|
|
||||||
*[Text(item.name, overflow="ellipsize") for item in order.items],
|
|
||||||
margin=0.5,
|
|
||||||
spacing=0.2
|
|
||||||
),
|
|
||||||
stroke_color=self.session.theme.primary_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
cursor="pointer",
|
|
||||||
hover_stroke_color=self.session.theme.warning_color,
|
|
||||||
hover_stroke_width=0.1,
|
|
||||||
min_width=30
|
|
||||||
),
|
|
||||||
on_press=lambda event, order=order: self.on_order_pressed(order),
|
|
||||||
) for order in self.open_orders],
|
|
||||||
Spacer(),
|
|
||||||
spacing=1,
|
|
||||||
margin=1
|
|
||||||
),
|
|
||||||
Spacer()
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
min_width=25,
|
|
||||||
grow_x=True,
|
|
||||||
margin_right=1
|
|
||||||
),
|
|
||||||
Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Text("Speisekarte", margin=0.5, selectable=False, overflow="wrap"),
|
|
||||||
fill=self.session.theme.header_box_background_color,
|
|
||||||
margin=0.4
|
|
||||||
),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
*[Column(
|
|
||||||
Text(text=category.value, margin_bottom=1, margin_top=0.5, fill=self.session.theme.primary_color),
|
|
||||||
*[Rectangle(
|
|
||||||
content=Row(Text(text=item.name, overflow="ellipsize", grow_x=True), Switch(is_on=item.active, on_change=lambda event, item=item: self.change_item_active(event, item)), margin=0.1),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color
|
|
||||||
) for item in filter(lambda i: i.category == category, self.all_menu_items)],
|
|
||||||
spacing=0.5
|
|
||||||
) for category in CateringMenuItemCategory],
|
|
||||||
spacing=0.5,
|
|
||||||
margin=1
|
|
||||||
),
|
|
||||||
Spacer()
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
min_width=25
|
|
||||||
),
|
|
||||||
margin=1
|
|
||||||
)
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Literal
|
|
||||||
|
|
||||||
from rio import Component, Column, Row, Text, page, Rectangle, SwitcherBar, SwitcherBarChangeEvent, List
|
|
||||||
|
|
||||||
from elm.components import CateringCategoryDisplay, CateringCart
|
|
||||||
from elm.types.CateringTypes import CateringMenuItem, CateringOrderedItem
|
|
||||||
|
|
||||||
|
|
||||||
@page(name="Catering", url_segment="catering")
|
|
||||||
class CateringPage(Component):
|
|
||||||
active_category: Literal["Frühstück", "Hauptspeisen", "Snacks & Dessert", "Softdrinks", "Alkohol"] = "Hauptspeisen"
|
|
||||||
cart: List[CateringOrderedItem] = List()
|
|
||||||
|
|
||||||
async def on_switcher_bar_change(self, event: SwitcherBarChangeEvent) -> None:
|
|
||||||
self.active_category = event.value
|
|
||||||
|
|
||||||
async def add_to_cart_pressed(self, item: CateringMenuItem, changed_options: dict[str, bool]) -> None:
|
|
||||||
order_item = CateringOrderedItem.from_menu_item(item, changed_options)
|
|
||||||
self.cart.append(order_item)
|
|
||||||
|
|
||||||
def build(self) -> Component:
|
|
||||||
return Row(
|
|
||||||
Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Rectangle(
|
|
||||||
content=SwitcherBar("Frühstück", "Hauptspeisen", "Snacks & Dessert", "Softdrinks", "Alkohol", margin=0.5, selected_value=self.bind().active_category, on_change=self.on_switcher_bar_change),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color
|
|
||||||
),
|
|
||||||
CateringCategoryDisplay(active_category=self.active_category, add_to_cart_pressed_callback=self.add_to_cart_pressed, grow_y=True),
|
|
||||||
grow_x=True,
|
|
||||||
spacing=1
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Text("Warenkorb", margin=0.5, selectable=False, overflow="wrap"),
|
|
||||||
fill=self.session.theme.header_box_background_color,
|
|
||||||
margin=0.4
|
|
||||||
),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color
|
|
||||||
),
|
|
||||||
CateringCart(cart=self.cart, grow_y=True),
|
|
||||||
spacing=1,
|
|
||||||
min_width=20
|
|
||||||
),
|
|
||||||
spacing=1,
|
|
||||||
margin=1
|
|
||||||
)
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from rio import Component, Column, Row, Text, Spacer, page, Rectangle
|
|
||||||
|
|
||||||
from elm.services import PreloadService
|
|
||||||
|
|
||||||
@page(name="FAQ", url_segment="faq")
|
|
||||||
class FaqPage(Component):
|
|
||||||
def build(self) -> Component:
|
|
||||||
return Row(
|
|
||||||
Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Text("F.A.Q.", margin=0.5, selectable=False, overflow="wrap"),
|
|
||||||
fill=self.session.theme.header_box_background_color,
|
|
||||||
margin=0.4
|
|
||||||
),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
),
|
|
||||||
*[Column(
|
|
||||||
Text(q_a[0], font_weight="bold", overflow="wrap", fill=self.session.theme.primary_color),
|
|
||||||
Text(q_a[1], italic=True, overflow="wrap"),
|
|
||||||
spacing=0.5,
|
|
||||||
margin_bottom=1.5,
|
|
||||||
margin=1
|
|
||||||
) for q_a in self.session[PreloadService].faq],
|
|
||||||
Spacer()
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color
|
|
||||||
),
|
|
||||||
grow_x=True,
|
|
||||||
grow_y=True,
|
|
||||||
margin=0 if self.session.is_mobile() else 1
|
|
||||||
)
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from hashlib import sha256
|
from _sha2 import sha256
|
||||||
from random import choices
|
from random import choices
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from rio import Component, Column, Row, Text, page, Rectangle, CodeBlock, Link
|
|
||||||
|
|
||||||
from elm.services import PreloadService
|
|
||||||
|
|
||||||
@page(name="Imprint", url_segment="imprint")
|
|
||||||
class ImprintPage(Component):
|
|
||||||
def build(self) -> Component:
|
|
||||||
return Row(
|
|
||||||
Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Text("Impressum & DSGVO", margin=0.5, selectable=False, overflow="wrap"),
|
|
||||||
fill=self.session.theme.header_box_background_color,
|
|
||||||
margin=0.4
|
|
||||||
),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
),
|
|
||||||
CodeBlock(
|
|
||||||
code=self.session[PreloadService].imprint,
|
|
||||||
show_controls=False,
|
|
||||||
language="text",
|
|
||||||
scroll_code_x="auto",
|
|
||||||
margin=1,
|
|
||||||
grow_y=True
|
|
||||||
),
|
|
||||||
Link(
|
|
||||||
content="Datenschutzerklärung",
|
|
||||||
target_url="https://ezgg-ev.de/privacy", # ToDo: Move into this app and make preloaded content
|
|
||||||
open_in_new_tab=True,
|
|
||||||
margin=1
|
|
||||||
)
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color
|
|
||||||
),
|
|
||||||
grow_x=True,
|
|
||||||
grow_y=True,
|
|
||||||
margin=0 if self.session.is_mobile() else 1
|
|
||||||
)
|
|
||||||
@@ -11,7 +11,7 @@ class LandingPage(Component):
|
|||||||
full_box = LandingPageBoxFull(
|
full_box = LandingPageBoxFull(
|
||||||
image_name="news_image.jpg",
|
image_name="news_image.jpg",
|
||||||
heading_text="EZGG LAN geht in die 2. Runde",
|
heading_text="EZGG LAN geht in die 2. Runde",
|
||||||
article_text="Am 04.06.2027 ist es soweit. Dann findet die EZGG LAN in der zweiten Edition statt. Es erwarten euch viele Verbesserungen zur letzten Edition und wir hoffen euch auch dieses mal begrüßen zu dürfen.",
|
article_text="Am 23.04.2027 ist es soweit. Dann findet die EZGG LAN in der zweiten Edition statt. Es erwarten euch viele Verbesserungen zur letzten Edition und wir hoffen euch auch dieses mal begrüßen zu dürfen.",
|
||||||
date="15.05.26"
|
date="15.05.26"
|
||||||
)
|
)
|
||||||
half_box_1 = LandingPageBoxHalf(
|
half_box_1 = LandingPageBoxHalf(
|
||||||
|
|||||||
@@ -32,9 +32,6 @@ class LoginPage(Component):
|
|||||||
self.login_in_progress = True
|
self.login_in_progress = True
|
||||||
user_name = copy(self.user_name) # Prevents race condition name swap
|
user_name = copy(self.user_name) # Prevents race condition name swap
|
||||||
is_valid = await self.session[UserService].is_login_valid(user_name, self.password)
|
is_valid = await self.session[UserService].is_login_valid(user_name, self.password)
|
||||||
if not is_valid: # Migrated users
|
|
||||||
user_name = user_name.lower().capitalize()
|
|
||||||
is_valid = await self.session[UserService].is_login_valid(user_name, self.password)
|
|
||||||
if is_valid:
|
if is_valid:
|
||||||
user: User = await self.session[UserService].get_user(user_name)
|
user: User = await self.session[UserService].get_user(user_name)
|
||||||
self.error_on_last_attempt = False
|
self.error_on_last_attempt = False
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from asyncio import sleep
|
from datetime import date, datetime
|
||||||
from datetime import datetime
|
|
||||||
from decimal import Decimal, ROUND_DOWN
|
from decimal import Decimal, ROUND_DOWN
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from rio import Component, Column, Row, Text, Spacer, page, Rectangle, GuardEvent, Revealer, Image, NumberInput
|
from rio import Component, Column, Row, Text, Spacer, page, Rectangle, TextInput, GuardEvent, DateInput, PointerEventListener, Revealer, Image, NumberInput
|
||||||
from rio.event import on_populate
|
from rio.event import on_populate
|
||||||
|
|
||||||
from elm.types import UserSession, Transaction
|
from elm.types import UserSession, Transaction
|
||||||
from elm.services import AccountingService
|
from elm.services import UserService, AccountingService
|
||||||
from elm.components import ElmButton
|
from elm.components import ElmButton, AvatarEditBox, AccountInfoBox
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ class TransactionRow(Component):
|
|||||||
return Rectangle(
|
return Rectangle(
|
||||||
content=Row(
|
content=Row(
|
||||||
Text(
|
Text(
|
||||||
f"{self.transaction_time.strftime('%d.%m.%y')} /",
|
f"{self.transaction_time.strftime("%d.%m.%y")} /",
|
||||||
justify="left",
|
justify="left",
|
||||||
font_size=0.8,
|
font_size=0.8,
|
||||||
margin_left=0.5,
|
margin_left=0.5,
|
||||||
@@ -85,30 +84,13 @@ class MyBalancePage(Component):
|
|||||||
"DE47517624340019856607",
|
"DE47517624340019856607",
|
||||||
f"AUFLADUNG - {self.session[UserSession].user_name}")
|
f"AUFLADUNG - {self.session[UserSession].user_name}")
|
||||||
|
|
||||||
async def check_if_paypal_process_done(self) -> None:
|
|
||||||
await sleep(2)
|
|
||||||
if await self.session[AccountingService].has_user_open_orders(self.session[UserSession].user_name):
|
|
||||||
self.session.create_task(self.check_if_paypal_process_done())
|
|
||||||
else:
|
|
||||||
self.paypal_charge_in_progress = False
|
|
||||||
self.paypal_charge_amount = 0.00
|
|
||||||
self.current_balance = self.session[AccountingService].make_euro_string_from_decimal(
|
|
||||||
await self.session[AccountingService].get_balance(self.session[UserSession].user_name)
|
|
||||||
)
|
|
||||||
self.last_20_transactions = (await self.session[AccountingService].get_transaction_history(self.session[UserSession].user_name))[:20]
|
|
||||||
self.paypal_revealer_open = False
|
|
||||||
|
|
||||||
async def pay_with_paypal(self) -> None:
|
async def pay_with_paypal(self) -> None:
|
||||||
self.paypal_charge_in_progress = True
|
self.paypal_charge_in_progress = True
|
||||||
logger.info("Starting PayPal transaction over %s for user %s", f"{self.paypal_charge_amount} €", self.session[UserSession].user_name)
|
logger.info("Starting PayPal transaction over %s for user %s", f"{self.paypal_charge_amount} €", self.session[UserSession].user_name)
|
||||||
amount = Decimal(self.paypal_charge_amount)
|
amount = Decimal(self.paypal_charge_amount)
|
||||||
try:
|
approval_url = await self.session[AccountingService].start_paypal_process(self.session[UserSession].user_name, amount)
|
||||||
approval_url = await self.session[AccountingService].start_paypal_process(self.session[UserSession].user_name, amount)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
return
|
|
||||||
self.session.open_url_in_browser(approval_url)
|
self.session.open_url_in_browser(approval_url)
|
||||||
self.session.create_task(self.check_if_paypal_process_done())
|
# ToDo: Catch return URL somehow and notify user
|
||||||
|
|
||||||
async def toggle_bank_revealer(self) -> None:
|
async def toggle_bank_revealer(self) -> None:
|
||||||
self.bank_revealer_open = not self.bank_revealer_open
|
self.bank_revealer_open = not self.bank_revealer_open
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from rio import Component, Column, Spacer, page, GuardEvent, Rectangle, Text, FlowContainer, Color, ProgressCircle
|
|
||||||
from rio.event import on_populate
|
|
||||||
|
|
||||||
from elm.services import UserService
|
|
||||||
from elm.types import UserSession
|
|
||||||
from elm.types.CateringTypes import CateringOrder, CateringOrderStatus
|
|
||||||
|
|
||||||
|
|
||||||
def my_orders_page_guard(event: GuardEvent) -> Optional[str]:
|
|
||||||
try:
|
|
||||||
_ = event.session[UserSession].user_name
|
|
||||||
return None
|
|
||||||
except KeyError:
|
|
||||||
return "/"
|
|
||||||
|
|
||||||
@page(name="My Orders", url_segment="my-orders", guard=my_orders_page_guard)
|
|
||||||
class MyOrdersPage(Component):
|
|
||||||
orders: list[CateringOrder] = []
|
|
||||||
is_loading: bool = True
|
|
||||||
|
|
||||||
@on_populate
|
|
||||||
async def on_populate(self) -> None:
|
|
||||||
user = await self.session[UserService].get_user(self.session[UserSession].user_name)
|
|
||||||
if not user:
|
|
||||||
self.is_loading = False
|
|
||||||
return
|
|
||||||
|
|
||||||
self.orders = await CateringOrder.find_many(CateringOrder.customer_id == user.id).to_list()
|
|
||||||
self.is_loading = False
|
|
||||||
|
|
||||||
def get_status_color(self, status: CateringOrderStatus) -> Color:
|
|
||||||
color = self.session.theme.warning_color
|
|
||||||
if status == CateringOrderStatus.DELAYED or status == CateringOrderStatus.CANCELED:
|
|
||||||
color = self.session.theme.danger_color
|
|
||||||
elif status == CateringOrderStatus.COMPLETED:
|
|
||||||
color = self.session.theme.success_color
|
|
||||||
return color
|
|
||||||
|
|
||||||
def build(self) -> Component:
|
|
||||||
if self.is_loading:
|
|
||||||
return ProgressCircle(margin=self.session.screen_width // 5)
|
|
||||||
return Column(
|
|
||||||
FlowContainer(
|
|
||||||
*[Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Text(f"Bestellung\n\n{str(order.id)[-5:]}", margin=0.5, selectable=False, overflow="wrap", justify="center"),
|
|
||||||
fill=self.session.theme.header_box_background_color,
|
|
||||||
margin=0.4
|
|
||||||
),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
*[Text(item.name, overflow="wrap") for item in order.items],
|
|
||||||
spacing=1,
|
|
||||||
margin=1
|
|
||||||
),
|
|
||||||
Spacer(),
|
|
||||||
Text(CateringOrder.translate_order_status(order.status), fill=self.get_status_color(order.status), margin=1, font_weight="bold", font_size=1, justify="center"),
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
min_width=18
|
|
||||||
) for order in self.orders],
|
|
||||||
spacing=1
|
|
||||||
),
|
|
||||||
Spacer(),
|
|
||||||
spacing=1,
|
|
||||||
margin=1
|
|
||||||
)
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from copy import copy
|
|
||||||
from typing import Any, Optional
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from rio import Component, Column, Row, Text, Spacer, page, Color, Rectangle, TextInput, GuardEvent
|
|
||||||
from rio.event import on_populate
|
|
||||||
|
|
||||||
from elm.types import UserSession, User, Ticket, Seat
|
|
||||||
from elm.services import UserService, LocalData, LocalDataService, ConfigurationService
|
|
||||||
from elm.components import ElmButton
|
|
||||||
|
|
||||||
@page(name="Participants", url_segment="participants")
|
|
||||||
class ParticipantsPage(Component):
|
|
||||||
participants: list[tuple[User, Seat]] = []
|
|
||||||
|
|
||||||
@on_populate
|
|
||||||
async def on_populate(self) -> None:
|
|
||||||
seats = await Seat.find_many(
|
|
||||||
Seat.user != None,
|
|
||||||
fetch_links=True
|
|
||||||
).to_list()
|
|
||||||
|
|
||||||
self.participants = [(seat.user, seat) for seat in seats]
|
|
||||||
|
|
||||||
def build(self) -> Component:
|
|
||||||
return Row(
|
|
||||||
Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Text("Teilnehmer", margin=0.5, selectable=False, overflow="wrap"),
|
|
||||||
fill=self.session.theme.header_box_background_color,
|
|
||||||
margin=0.4
|
|
||||||
),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
Row(
|
|
||||||
Text("Nutzer", grow_x=True, font_weight="bold"),
|
|
||||||
Text("Sitzplatz", font_weight="bold"),
|
|
||||||
margin=0.5
|
|
||||||
),
|
|
||||||
*[
|
|
||||||
Rectangle(
|
|
||||||
content=Row(
|
|
||||||
Text(user.user_name, grow_x=True, font_size=0.8),
|
|
||||||
Text(seat.seat_id, font_size=0.8),
|
|
||||||
margin=0.5
|
|
||||||
),
|
|
||||||
hover_fill=self.session.theme.secondary_color,
|
|
||||||
transition_time=0.2
|
|
||||||
) for user, seat in self.participants],
|
|
||||||
margin=1
|
|
||||||
),
|
|
||||||
Spacer()
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color
|
|
||||||
),
|
|
||||||
margin=1,
|
|
||||||
grow_x=True
|
|
||||||
)
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from asyncio import sleep
|
|
||||||
|
|
||||||
from rio import Component, Column, Row, Text, Spacer, page, Rectangle, QueryParameter, ProgressCircle
|
|
||||||
from rio.event import on_populate
|
|
||||||
|
|
||||||
from elm.services import AccountingService
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
|
||||||
|
|
||||||
@page(name="PayPal Return", url_segment="return-paypal")
|
|
||||||
class PayPalReturnPage(Component):
|
|
||||||
token: QueryParameter[str] = "No Value"
|
|
||||||
in_progress: bool = True
|
|
||||||
error_message: str = ""
|
|
||||||
success_message: str = ""
|
|
||||||
|
|
||||||
@on_populate
|
|
||||||
async def on_populate(self) -> None:
|
|
||||||
result = await self.session[AccountingService].finalize_paypal_process(self.token)
|
|
||||||
await sleep(1)
|
|
||||||
if result:
|
|
||||||
self.in_progress = False
|
|
||||||
self.success_message = "Aufladung erfolgreich. Du kannst dieses Fenster schließen."
|
|
||||||
else:
|
|
||||||
self.in_progress = False
|
|
||||||
self.error_message = "Es ist ein Fehler aufgetreten, bitte kontaktiere uns"
|
|
||||||
|
|
||||||
def build(self) -> Component:
|
|
||||||
col_contents = []
|
|
||||||
if self.in_progress:
|
|
||||||
col_contents.append(ProgressCircle(min_size=5, color=self.session.theme.primary_color))
|
|
||||||
col_contents.append(Text("Wir prüfen deine Aufladung", overflow="wrap", justify="center"))
|
|
||||||
else:
|
|
||||||
if self.error_message:
|
|
||||||
col_contents.append(Text(self.error_message, overflow="wrap", justify="center", fill=self.session.theme.danger_color))
|
|
||||||
elif self.success_message:
|
|
||||||
col_contents.append(Text(self.success_message, overflow="wrap", justify="center", fill=self.session.theme.success_color))
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Text("Paypal Aufladung", margin=0.5, selectable=False, overflow="wrap"),
|
|
||||||
fill=self.session.theme.header_box_background_color,
|
|
||||||
margin=0.4
|
|
||||||
),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
*col_contents,
|
|
||||||
margin=1,
|
|
||||||
spacing=1
|
|
||||||
),
|
|
||||||
Spacer()
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
min_width=1 if self.session.is_mobile() else 25
|
|
||||||
),
|
|
||||||
align_x=0.5,
|
|
||||||
align_y=0.5
|
|
||||||
)
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from rio import Component, Column, Text, page, Rectangle
|
|
||||||
|
|
||||||
from elm.services import PreloadService
|
|
||||||
|
|
||||||
|
|
||||||
@page(name="Rules", url_segment="rules")
|
|
||||||
class RulesPage(Component):
|
|
||||||
def build(self) -> Component:
|
|
||||||
return Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Text("Regeln", margin=0.5, selectable=False, overflow="wrap"),
|
|
||||||
fill=self.session.theme.header_box_background_color,
|
|
||||||
margin=0.4
|
|
||||||
),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
),
|
|
||||||
*[Column(
|
|
||||||
Text(
|
|
||||||
rule.split(":")[0],
|
|
||||||
font_size=0.8 if self.session.is_mobile() else 1.2,
|
|
||||||
fill=self.session.theme.primary_color,
|
|
||||||
overflow="wrap"
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
rule.split(":")[1].strip(),
|
|
||||||
font_size=0.7 if self.session.is_mobile() else 1,
|
|
||||||
overflow="wrap",
|
|
||||||
margin_left=1
|
|
||||||
),
|
|
||||||
margin=1,
|
|
||||||
spacing=1
|
|
||||||
) for rule in self.session[PreloadService].rules]
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color
|
|
||||||
),
|
|
||||||
Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Text("AGB", margin=0.5, selectable=False, overflow="wrap"),
|
|
||||||
fill=self.session.theme.header_box_background_color,
|
|
||||||
margin=0.4
|
|
||||||
),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
),
|
|
||||||
*[Column(
|
|
||||||
Text(
|
|
||||||
text=paragraph,
|
|
||||||
font_size=0.8 if self.session.is_mobile() else 1.2,
|
|
||||||
fill=self.session.theme.primary_color,
|
|
||||||
overflow="wrap"
|
|
||||||
),
|
|
||||||
*[Text(
|
|
||||||
section,
|
|
||||||
font_size=0.7 if self.session.is_mobile() else 1,
|
|
||||||
overflow="wrap",
|
|
||||||
margin_left=1
|
|
||||||
) for section in self.session[PreloadService].gtc[paragraph]],
|
|
||||||
margin=1,
|
|
||||||
spacing=1
|
|
||||||
) for paragraph in self.session[PreloadService].gtc]
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color
|
|
||||||
),
|
|
||||||
grow_x=True,
|
|
||||||
grow_y=True,
|
|
||||||
spacing=1,
|
|
||||||
margin=0 if self.session.is_mobile() else 1
|
|
||||||
)
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from bson import ObjectId
|
|
||||||
from rio import Component, page, Rectangle, ProgressCircle, Row, QueryParameter, Column, Text, Spacer
|
|
||||||
from rio.event import on_populate
|
|
||||||
|
|
||||||
from elm import UserSession
|
|
||||||
from elm.components import ElmButton
|
|
||||||
from elm.types import Seat, User, Ticket
|
|
||||||
|
|
||||||
|
|
||||||
@page(name="Seat Info", url_segment="seat-info")
|
|
||||||
class SeatInfoPage(Component):
|
|
||||||
seat_id: QueryParameter[str] = ""
|
|
||||||
seat: Optional[Seat] = None
|
|
||||||
seat_user: Optional[User] = None
|
|
||||||
initial_load_done: bool = False
|
|
||||||
choosing_button_loading: bool = False
|
|
||||||
message: str = ""
|
|
||||||
message_is_error: bool = False
|
|
||||||
|
|
||||||
@on_populate
|
|
||||||
async def on_populate(self) -> None:
|
|
||||||
self.seat = await Seat.find_one(Seat.seat_id == self.seat_id)
|
|
||||||
if self.seat and self.seat.user is not None:
|
|
||||||
self.seat_user = await self.seat.user.fetch()
|
|
||||||
self.initial_load_done = True
|
|
||||||
|
|
||||||
async def choose_seat(self) -> None:
|
|
||||||
self.choosing_button_loading = True
|
|
||||||
try:
|
|
||||||
user_name = self.session[UserSession].user_name
|
|
||||||
except KeyError:
|
|
||||||
self.session.navigate_to("./login")
|
|
||||||
return
|
|
||||||
|
|
||||||
user = await User.find_one(User.user_name == user_name)
|
|
||||||
if not user:
|
|
||||||
return
|
|
||||||
|
|
||||||
user_ticket = await Ticket.find_one(
|
|
||||||
{"owner.$id": ObjectId(user.id)}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not user_ticket or user_ticket.category != self.seat.category:
|
|
||||||
self.message = "Du hast nicht das passende Ticket"
|
|
||||||
self.message_is_error = True
|
|
||||||
self.choosing_button_loading = False
|
|
||||||
return
|
|
||||||
|
|
||||||
user_seat = await Seat.find_one(
|
|
||||||
{"user.$id": ObjectId(user.id)}
|
|
||||||
)
|
|
||||||
|
|
||||||
if user_seat is not None:
|
|
||||||
self.message = "Du hast bereits einen Sitzplatz"
|
|
||||||
self.message_is_error = True
|
|
||||||
self.choosing_button_loading = False
|
|
||||||
return
|
|
||||||
|
|
||||||
s = await Seat.find_one(Seat.seat_id == self.seat_id)
|
|
||||||
if not s:
|
|
||||||
return
|
|
||||||
s.user = user
|
|
||||||
await s.save()
|
|
||||||
self.message = "Sitzplatz gewählt!"
|
|
||||||
self.message_is_error = False
|
|
||||||
self.choosing_button_loading = False
|
|
||||||
|
|
||||||
def build(self) -> Component:
|
|
||||||
if not self.initial_load_done:
|
|
||||||
box_contents = [ProgressCircle(margin=1)]
|
|
||||||
else:
|
|
||||||
if self.seat is None:
|
|
||||||
box_contents = [Text(text="Der angeforderte Sitzplatz konnte nicht gefunden werden", margin=1, overflow="wrap", justify="center", fill=self.session.theme.danger_color)]
|
|
||||||
else:
|
|
||||||
box_contents = [
|
|
||||||
Row(
|
|
||||||
Text(text="Kategorie:", justify="left"),
|
|
||||||
Text(text=self.seat.category, justify="right"),
|
|
||||||
spacing=1
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
Text(text="Belegt:", justify="left"),
|
|
||||||
Text(text="Ja" if self.seat.user is not None or self.seat.is_blocked else "Nein", justify="right", fill=self.session.theme.danger_color if self.seat.user is not None or self.seat.is_blocked else self.session.theme.success_color),
|
|
||||||
spacing=1
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
Text(text="Nutzer:", justify="left"),
|
|
||||||
Text(text=self.seat_user.user_name if self.seat_user else "-", justify="right"),
|
|
||||||
spacing=1
|
|
||||||
),
|
|
||||||
]
|
|
||||||
if not self.seat.is_blocked and self.seat.user is None:
|
|
||||||
box_contents.append(
|
|
||||||
ElmButton(text="Platz wählen", on_press=self.choose_seat, is_loading=self.choosing_button_loading)
|
|
||||||
)
|
|
||||||
box_contents.append(
|
|
||||||
Text(text=self.message, fill=self.session.theme.danger_color if self.message_is_error else self.session.theme.success_color, overflow="wrap", justify="center")
|
|
||||||
)
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Text(f"Sitzplatz: {self.seat_id}", margin=0.5, selectable=False),
|
|
||||||
fill=self.session.theme.header_box_background_color,
|
|
||||||
margin=0.4
|
|
||||||
),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
*box_contents,
|
|
||||||
margin=1,
|
|
||||||
spacing=1
|
|
||||||
),
|
|
||||||
Spacer()
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color
|
|
||||||
),
|
|
||||||
align_x=0.5,
|
|
||||||
align_y=0.5
|
|
||||||
)
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from rio import Component, page, Rectangle, PointerEvent, ProgressCircle, Row
|
|
||||||
from rio.event import on_populate
|
|
||||||
|
|
||||||
from elm.components import SeatingPlan
|
|
||||||
from elm.types import Seat
|
|
||||||
|
|
||||||
|
|
||||||
@page(name="Seating Plan", url_segment="seating")
|
|
||||||
class SeatingPlanPage(Component):
|
|
||||||
preloaded_seats: Optional[list[Seat]] = None
|
|
||||||
|
|
||||||
@on_populate
|
|
||||||
async def on_populate(self) -> None:
|
|
||||||
self.preloaded_seats = await Seat.find_all().to_list()
|
|
||||||
|
|
||||||
|
|
||||||
def build(self) -> Component:
|
|
||||||
return Rectangle(
|
|
||||||
content=Row(ProgressCircle(), margin=self.session.screen_width // 6) if self.preloaded_seats is None else SeatingPlan(margin=0, preloaded_seats=self.preloaded_seats),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width = 0.1,
|
|
||||||
stroke_color = self.session.theme.box_border_color,
|
|
||||||
margin_left=1,
|
|
||||||
margin_top=1
|
|
||||||
)
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from rio import Component, Column, Row, page
|
|
||||||
|
|
||||||
from elm.services import ConfigurationService
|
|
||||||
from elm.components import BuyTicketBox
|
|
||||||
|
|
||||||
@page(name="Tickets", url_segment="tickets")
|
|
||||||
class TicketsPage(Component):
|
|
||||||
def build(self) -> Component:
|
|
||||||
row_col = Column if self.session.is_mobile() else Row
|
|
||||||
ticket_boxes = []
|
|
||||||
for ticket_info in self.session[ConfigurationService].get_ticket_info():
|
|
||||||
ticket_boxes.append(BuyTicketBox(ticket_info=ticket_info))
|
|
||||||
|
|
||||||
return row_col(
|
|
||||||
*ticket_boxes,
|
|
||||||
spacing=1,
|
|
||||||
margin=1
|
|
||||||
)
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from copy import copy
|
|
||||||
from typing import Any, Optional
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from rio import Component, Column, Row, Text, Spacer, page, Color, Rectangle, TextInput, GuardEvent
|
|
||||||
|
|
||||||
from elm.types import UserSession, User
|
|
||||||
from elm.services import UserService, LocalData, LocalDataService, ConfigurationService
|
|
||||||
from elm.components import ElmButton
|
|
||||||
|
|
||||||
|
|
||||||
@page(name="Tournaments", url_segment="tournaments")
|
|
||||||
class TournamentsPage(Component):
|
|
||||||
def build(self) -> Component:
|
|
||||||
return Row(
|
|
||||||
Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Text("Turniere", margin=0.5, selectable=False, overflow="wrap"),
|
|
||||||
fill=self.session.theme.header_box_background_color,
|
|
||||||
margin=0.4
|
|
||||||
),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
Text("Es wurde noch kein Turnierbaum hinterlegt", fill=self.session.theme.text_color, overflow="wrap", justify="center"),
|
|
||||||
margin=1,
|
|
||||||
spacing=1
|
|
||||||
),
|
|
||||||
Spacer()
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color
|
|
||||||
),
|
|
||||||
align_x=0.5,
|
|
||||||
align_y=0.5
|
|
||||||
)
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from copy import copy
|
|
||||||
from functools import partial
|
|
||||||
from typing import Optional
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from beanie.odm.interfaces.find import FindInterface
|
|
||||||
from bson import ObjectId
|
|
||||||
from rio import Component, Column, Row, Text, Spacer, page, Rectangle, TextInput, GuardEvent, Button, TextInputChangeEvent, NumberInput, IconButton
|
|
||||||
from rio.event import on_populate
|
|
||||||
|
|
||||||
from elm.types import UserSession, User, Transaction, Ticket, Seat
|
|
||||||
from elm.services import AccountingService, MailingService, ConfigurationService
|
|
||||||
from elm.components import AccountInfoBox
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
|
||||||
|
|
||||||
def user_admin_page_guard(event: GuardEvent) -> Optional[str]:
|
|
||||||
try:
|
|
||||||
if event.session[UserSession].is_team_member:
|
|
||||||
return None
|
|
||||||
return "/"
|
|
||||||
except KeyError:
|
|
||||||
return "/"
|
|
||||||
|
|
||||||
@page(name="Benutzerverwaltung", url_segment="user-admin", guard=user_admin_page_guard)
|
|
||||||
class UserAdminPage(Component):
|
|
||||||
all_users: list[User] = list()
|
|
||||||
user_list: list[User] = list()
|
|
||||||
search_bar_text: str = ""
|
|
||||||
active_user: Optional[User] = None
|
|
||||||
transaction_value: float = 0.0
|
|
||||||
transaction_reason: str = ""
|
|
||||||
active_user_balance: str = "0.00 €"
|
|
||||||
|
|
||||||
@on_populate
|
|
||||||
async def on_populate(self) -> None:
|
|
||||||
user_list = await User.find_all().to_list()
|
|
||||||
self.all_users = sorted(user_list, key=lambda u: u.user_name)
|
|
||||||
self.user_list = sorted(user_list, key=lambda u: u.user_name)
|
|
||||||
|
|
||||||
async def on_search_bar_text_changed(self, e: TextInputChangeEvent) -> None:
|
|
||||||
self.user_list = list(filter(lambda user: (e.text.lower() in user.user_name.lower()), self.all_users))
|
|
||||||
|
|
||||||
async def on_user_clicked(self, user: User) -> None:
|
|
||||||
self.active_user = user
|
|
||||||
self.active_user_balance = self.session[AccountingService].make_euro_string_from_decimal(
|
|
||||||
await self.session[AccountingService].get_balance(user.user_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def create_debit_transaction(self) -> None:
|
|
||||||
if not self.active_user:
|
|
||||||
return
|
|
||||||
logger.info(f"Trying to remove {self.transaction_value} to {self.active_user.user_name} ({self.transaction_reason})")
|
|
||||||
new_transaction = Transaction(
|
|
||||||
user_name=self.active_user.user_name,
|
|
||||||
value=Decimal(str(self.transaction_value)),
|
|
||||||
is_debit=True,
|
|
||||||
title=self.transaction_reason
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
await new_transaction.save()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
|
|
||||||
self.transaction_value = 0.0
|
|
||||||
self.transaction_reason = ""
|
|
||||||
self.active_user_balance = self.session[AccountingService].make_euro_string_from_decimal(
|
|
||||||
await self.session[AccountingService].get_balance(self.active_user.user_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def create_credit_transaction(self) -> None:
|
|
||||||
if not self.active_user:
|
|
||||||
return
|
|
||||||
logger.info(f"Trying to add {self.transaction_value} to {self.active_user.user_name} ({self.transaction_reason})")
|
|
||||||
value = Decimal(str(self.transaction_value))
|
|
||||||
new_transaction = Transaction(
|
|
||||||
user_name=self.active_user.user_name,
|
|
||||||
value=value,
|
|
||||||
is_debit=False,
|
|
||||||
title=self.transaction_reason
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
await new_transaction.save()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
|
|
||||||
self.transaction_value = 0.0
|
|
||||||
self.transaction_reason = ""
|
|
||||||
total_balance = await self.session[AccountingService].get_balance(self.active_user.user_name)
|
|
||||||
self.active_user_balance = self.session[AccountingService].make_euro_string_from_decimal(total_balance)
|
|
||||||
|
|
||||||
self.session.create_task(self.session[MailingService].send_email(
|
|
||||||
subject="Dein Guthaben wurde aufgeladen!",
|
|
||||||
body=self.session[MailingService].generate_account_balance_added_mail_body(user=self.active_user, added_balance=value, total_balance=total_balance),
|
|
||||||
receiver=self.active_user.user_mail
|
|
||||||
))
|
|
||||||
|
|
||||||
async def cancel_ticket(self) -> None:
|
|
||||||
if self.active_user is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
ticket = await Ticket.find_one({"owner.$id": self.active_user.id})
|
|
||||||
if ticket is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
ticket_price = Decimal(0)
|
|
||||||
for ticket_info in self.session[ConfigurationService].get_ticket_info():
|
|
||||||
if ticket_info.category == ticket.category:
|
|
||||||
ticket_price = ticket_info.price
|
|
||||||
|
|
||||||
await ticket.delete()
|
|
||||||
await self.session[AccountingService].add_balance(self.active_user.user_name, ticket_price, "TICKET STORNO", skip_mail=True)
|
|
||||||
await self.free_seat()
|
|
||||||
|
|
||||||
async def free_seat(self) -> None:
|
|
||||||
if self.active_user is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
seat = await Seat.find_one({"user.$id": ObjectId(self.active_user.id)})
|
|
||||||
if seat is None:
|
|
||||||
self.active_user = None
|
|
||||||
return
|
|
||||||
|
|
||||||
seat.user = None
|
|
||||||
await seat.save()
|
|
||||||
self.active_user = None
|
|
||||||
|
|
||||||
def build(self) -> Component:
|
|
||||||
right_panel_contents = []
|
|
||||||
if not self.active_user:
|
|
||||||
right_panel_contents.append(Spacer())
|
|
||||||
else:
|
|
||||||
right_panel_contents.extend([
|
|
||||||
AccountInfoBox(fixed_user=self.active_user),
|
|
||||||
Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Text(f"LAN Konto - Kontostand: {self.active_user_balance}", margin=0.5, selectable=False, overflow="wrap"),
|
|
||||||
fill=self.session.theme.header_box_background_color,
|
|
||||||
margin=0.4
|
|
||||||
),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
NumberInput(
|
|
||||||
value=self.bind().transaction_value,
|
|
||||||
label="Betrag",
|
|
||||||
suffix_text="€",
|
|
||||||
decimals=2,
|
|
||||||
margin=1,
|
|
||||||
margin_bottom=0
|
|
||||||
),
|
|
||||||
TextInput(
|
|
||||||
text=self.bind().transaction_reason,
|
|
||||||
label="Beschreibung",
|
|
||||||
margin=1,
|
|
||||||
margin_bottom=0
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
Button(
|
|
||||||
content="Entfernen",
|
|
||||||
shape="rectangle",
|
|
||||||
color="danger",
|
|
||||||
margin=1,
|
|
||||||
on_press=self.create_debit_transaction
|
|
||||||
),
|
|
||||||
Button(
|
|
||||||
content="Hinzufügen",
|
|
||||||
shape="rectangle",
|
|
||||||
color="success",
|
|
||||||
margin=1,
|
|
||||||
on_press=self.create_credit_transaction
|
|
||||||
)
|
|
||||||
),
|
|
||||||
margin=1,
|
|
||||||
spacing=1
|
|
||||||
),
|
|
||||||
Spacer()
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color
|
|
||||||
),
|
|
||||||
Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Text(f"Sonstiges", margin=0.5,
|
|
||||||
selectable=False, overflow="wrap"),
|
|
||||||
fill=self.session.theme.header_box_background_color,
|
|
||||||
margin=0.4
|
|
||||||
),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
Row(
|
|
||||||
Button(
|
|
||||||
content="Ticket stornieren",
|
|
||||||
shape="rectangle",
|
|
||||||
color="danger",
|
|
||||||
margin=1,
|
|
||||||
on_press=self.cancel_ticket
|
|
||||||
),
|
|
||||||
Button(
|
|
||||||
content="Sitzplatz freigeben",
|
|
||||||
shape="rectangle",
|
|
||||||
color="danger",
|
|
||||||
margin=1,
|
|
||||||
on_press=self.free_seat
|
|
||||||
)
|
|
||||||
),
|
|
||||||
margin=1,
|
|
||||||
spacing=1
|
|
||||||
),
|
|
||||||
Spacer()
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color
|
|
||||||
),
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Text("Nutzerliste", margin=0.5, selectable=False, overflow="wrap"),
|
|
||||||
fill=self.session.theme.header_box_background_color,
|
|
||||||
margin=0.4
|
|
||||||
),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
TextInput(label="Nutzername", text=self.bind().search_bar_text, on_change=self.on_search_bar_text_changed, margin_bottom=1),
|
|
||||||
*[Button(content=user.user_name, shape="rectangle", style="plain-text", on_press=partial(self.on_user_clicked, user)) for user in self.user_list],
|
|
||||||
margin=1
|
|
||||||
),
|
|
||||||
Spacer()
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
min_width=25
|
|
||||||
),
|
|
||||||
Rectangle(
|
|
||||||
content=Column(
|
|
||||||
Rectangle(
|
|
||||||
content=Rectangle(
|
|
||||||
content=Row(
|
|
||||||
Text(f"Nutzer bearbeiten{': ' if self.active_user else ''}{self.active_user.user_name if self.active_user else ''}", margin=0.5, selectable=False, overflow="wrap", grow_x=True),
|
|
||||||
IconButton("material/cancel", min_size=1.5, color="primary", margin_right=1, on_press=lambda: self.__setattr__("active_user", None)),
|
|
||||||
),
|
|
||||||
fill=self.session.theme.header_box_background_color,
|
|
||||||
margin=0.4
|
|
||||||
),
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
*right_panel_contents,
|
|
||||||
margin=1,
|
|
||||||
spacing=1
|
|
||||||
),
|
|
||||||
Spacer()
|
|
||||||
),
|
|
||||||
fill=self.session.theme.box_color,
|
|
||||||
stroke_width=0.1,
|
|
||||||
stroke_color=self.session.theme.box_border_color,
|
|
||||||
grow_x=True
|
|
||||||
),
|
|
||||||
spacing=1,
|
|
||||||
margin=1
|
|
||||||
)
|
|
||||||
@@ -7,7 +7,6 @@ import httpx
|
|||||||
import qrcode
|
import qrcode
|
||||||
|
|
||||||
from elm.types import Transaction, User, PayPalConfiguration
|
from elm.types import Transaction, User, PayPalConfiguration
|
||||||
from elm.services import MailingService, ConfigurationService
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
|
|
||||||
@@ -17,29 +16,16 @@ class InsufficientFundsError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class AccountingService:
|
class AccountingService:
|
||||||
PAYPAL_SANDBOX_URL = "https://api-m.sandbox.paypal.com"
|
def __init__(self, paypal_config: PayPalConfiguration) -> None:
|
||||||
PAYPAL_PROD_URL = "https://api-m.paypal.com"
|
self._paypal_config = paypal_config
|
||||||
|
|
||||||
def __init__(self, configuration_service: ConfigurationService, mailing_service: MailingService) -> None:
|
|
||||||
self._configuration_service = configuration_service
|
|
||||||
self._paypal_config: PayPalConfiguration = configuration_service.get_paypal_configuration()
|
|
||||||
self._pending_paypal_orders: dict[str, tuple[str, Decimal]] = {}
|
|
||||||
self._mailing_service = mailing_service
|
|
||||||
|
|
||||||
async def has_user_open_orders(self, user_name: str) -> bool:
|
|
||||||
for pending_paypal_order in self._pending_paypal_orders.values():
|
|
||||||
if pending_paypal_order[0] == user_name:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def get_paypal_access_token(self) -> str:
|
async def get_paypal_access_token(self) -> str:
|
||||||
url = self.PAYPAL_SANDBOX_URL if self._configuration_service.DEV_MODE_ACTIVE else self.PAYPAL_PROD_URL
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
f"{url}/v1/oauth2/token",
|
"https://api-m.sandbox.paypal.com/v1/oauth2/token",
|
||||||
auth=(
|
auth=(
|
||||||
self._paypal_config.client_id_sandbox if self._configuration_service.DEV_MODE_ACTIVE else self._paypal_config.client_id,
|
self._paypal_config.client_id_sandbox,
|
||||||
self._paypal_config.secret_sandbox if self._configuration_service.DEV_MODE_ACTIVE else self._paypal_config.secret,
|
self._paypal_config.secret_sandbox,
|
||||||
),
|
),
|
||||||
headers={
|
headers={
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
@@ -55,13 +41,10 @@ class AccountingService:
|
|||||||
return data["access_token"]
|
return data["access_token"]
|
||||||
|
|
||||||
async def start_paypal_process(self, user_name: str, amount: Decimal) -> str:
|
async def start_paypal_process(self, user_name: str, amount: Decimal) -> str:
|
||||||
url = self.PAYPAL_SANDBOX_URL if self._configuration_service.DEV_MODE_ACTIVE else self.PAYPAL_PROD_URL
|
|
||||||
return_domain = "http://localhost:8000" if self._configuration_service.DEV_MODE_ACTIVE else self._configuration_service.BASE_URL
|
|
||||||
amount = amount.quantize(Decimal(".01"))
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
access_token = await self.get_paypal_access_token()
|
access_token = await self.get_paypal_access_token()
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
url=f"{url}/v2/checkout/orders/",
|
url="https://api-m.sandbox.paypal.com/v2/checkout/orders/",
|
||||||
headers={
|
headers={
|
||||||
"Authorization": f"Bearer {access_token}"
|
"Authorization": f"Bearer {access_token}"
|
||||||
},
|
},
|
||||||
@@ -75,62 +58,18 @@ class AccountingService:
|
|||||||
"value": str(amount)
|
"value": str(amount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"payment_source": {
|
|
||||||
"paypal": {
|
|
||||||
"experience_context": {
|
|
||||||
"return_url": f"{return_domain}/return-paypal",
|
|
||||||
"cancel_url": f"{return_domain}/cancel-paypal",
|
|
||||||
"user_action": "PAY_NOW",
|
|
||||||
"shipping_preference": "NO_SHIPPING"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
try:
|
approval_url = next(
|
||||||
payer_action_url = next(
|
link["href"]
|
||||||
link["href"]
|
for link in response.json()["links"]
|
||||||
for link in response.json()["links"]
|
if link["rel"] == "approve"
|
||||||
if link["rel"] == "payer-action"
|
|
||||||
)
|
|
||||||
except StopIteration:
|
|
||||||
logger.error("No payer action url found: %s", response.text)
|
|
||||||
return "#"
|
|
||||||
|
|
||||||
self._pending_paypal_orders[response.json()["id"]] = (user_name, amount)
|
|
||||||
|
|
||||||
return payer_action_url
|
|
||||||
|
|
||||||
async def finalize_paypal_process(self, order_id: str) -> bool:
|
|
||||||
url = self.PAYPAL_SANDBOX_URL if self._configuration_service.DEV_MODE_ACTIVE else self.PAYPAL_PROD_URL
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
access_token = await self.get_paypal_access_token()
|
|
||||||
response = await client.get(
|
|
||||||
url=f"{url}/v2/checkout/orders/{order_id}",
|
|
||||||
headers={
|
|
||||||
"Authorization": f"Bearer {access_token}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
is_approved = response.json()["status"] == "APPROVED"
|
return approval_url
|
||||||
|
|
||||||
if is_approved:
|
|
||||||
response = await client.post(
|
|
||||||
f"{url}/v2/checkout/orders/{order_id}/capture",
|
|
||||||
headers={
|
|
||||||
"Authorization": f"Bearer {access_token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
is_completed = response.json()["status"] == "COMPLETED"
|
|
||||||
if is_completed:
|
|
||||||
await self.add_balance(self._pending_paypal_orders[order_id][0], self._pending_paypal_orders[order_id][1], "PayPal Aufladung")
|
|
||||||
self._pending_paypal_orders.pop(order_id)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def add_balance(self, user_name: str, balance_to_add: Decimal, title: str, skip_mail: bool = False) -> Decimal:
|
async def add_balance(self, user_name: str, balance_to_add: Decimal, title: str) -> Decimal:
|
||||||
user = await User.find_one(User.user_name == user_name)
|
user = await User.find_one(User.user_name == user_name)
|
||||||
if not user:
|
if not user:
|
||||||
raise KeyError("User does not exist")
|
raise KeyError("User does not exist")
|
||||||
@@ -141,14 +80,7 @@ class AccountingService:
|
|||||||
title=title
|
title=title
|
||||||
).save()
|
).save()
|
||||||
logger.debug(f"Added balance of {self.make_euro_string_from_decimal(balance_to_add)} to user '{user_name}'")
|
logger.debug(f"Added balance of {self.make_euro_string_from_decimal(balance_to_add)} to user '{user_name}'")
|
||||||
new_balance = await self.get_balance(user_name)
|
return await self.get_balance(user_name)
|
||||||
if not skip_mail:
|
|
||||||
await self._mailing_service.send_email(
|
|
||||||
"Dein Guthaben wurde aufgeladen",
|
|
||||||
self._mailing_service.generate_account_balance_added_mail_body(user, balance_to_add, new_balance),
|
|
||||||
user.user_mail
|
|
||||||
)
|
|
||||||
return new_balance
|
|
||||||
|
|
||||||
async def remove_balance(self, user_name: str, balance_to_remove: Decimal, title: str) -> Decimal:
|
async def remove_balance(self, user_name: str, balance_to_remove: Decimal, title: str) -> Decimal:
|
||||||
current_balance = await self.get_balance(user_name)
|
current_balance = await self.get_balance(user_name)
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import logging
|
import logging
|
||||||
import tomllib
|
import tomllib
|
||||||
|
|
||||||
from from_root import from_root
|
from from_root import from_root
|
||||||
|
|
||||||
from elm.types.ConfigurationTypes import MailingServiceConfiguration, LanInfo, ReceiptPrintingConfiguration, DatabaseConfiguration, PayPalConfiguration, TicketInfo
|
from elm.types.ConfigurationTypes import MailingServiceConfiguration, LanInfo, ReceiptPrintingConfiguration, DatabaseConfiguration, PayPalConfiguration
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
@@ -47,30 +46,11 @@ class ConfigurationService:
|
|||||||
logger.fatal("Error loading DatabaseConfiguration, exiting...")
|
logger.fatal("Error loading DatabaseConfiguration, exiting...")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
def get_ticket_info(self) -> tuple[TicketInfo, ...]:
|
|
||||||
try:
|
|
||||||
return tuple([TicketInfo(
|
|
||||||
category=value,
|
|
||||||
total_tickets=self._config["tickets"][value]["total_tickets"],
|
|
||||||
price=Decimal(self._config["tickets"][value]["price"]),
|
|
||||||
description=self._config["tickets"][value]["description"],
|
|
||||||
additional_info=self._config["tickets"][value]["additional_info"],
|
|
||||||
can_be_sold=self._config["tickets"][value]["can_be_sold"]
|
|
||||||
) for value in self._config["tickets"]])
|
|
||||||
except KeyError as e:
|
|
||||||
logger.debug(e)
|
|
||||||
logger.fatal("Error loading ticket configuration, exiting...")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def get_database_configuration(self) -> DatabaseConfiguration:
|
def get_database_configuration(self) -> DatabaseConfiguration:
|
||||||
try:
|
try:
|
||||||
return DatabaseConfiguration(
|
return DatabaseConfiguration(
|
||||||
database_host=self._config["database"]["database_host"],
|
database_address=self._config["database"]["database_address"],
|
||||||
database_port=self._config["database"]["database_port"],
|
|
||||||
database_name=self._config["database"]["database_name"],
|
database_name=self._config["database"]["database_name"],
|
||||||
database_user=self._config["database"]["database_user"],
|
|
||||||
database_password=self._config["database"]["database_password"],
|
|
||||||
)
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logger.fatal("Error loading DatabaseConfiguration, exiting...")
|
logger.fatal("Error loading DatabaseConfiguration, exiting...")
|
||||||
@@ -103,9 +83,7 @@ class ConfigurationService:
|
|||||||
has_wifi=lan_info["has_wifi"],
|
has_wifi=lan_info["has_wifi"],
|
||||||
has_showers=lan_info["has_showers"],
|
has_showers=lan_info["has_showers"],
|
||||||
ts3_address=lan_info["ts3_address"],
|
ts3_address=lan_info["ts3_address"],
|
||||||
discord_invite_link=lan_info["discord_invite_link"],
|
discord_invite_link=lan_info["discord_invite_link"]
|
||||||
location_name=lan_info["location_name"],
|
|
||||||
location_link=lan_info["location_link"]
|
|
||||||
)
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logger.fatal("Error loading LAN Info, exiting...")
|
logger.fatal("Error loading LAN Info, exiting...")
|
||||||
@@ -139,7 +117,3 @@ class ConfigurationService:
|
|||||||
@property
|
@property
|
||||||
def DEFAULT_PROFILE_PICTURE(self) -> bytes:
|
def DEFAULT_PROFILE_PICTURE(self) -> bytes:
|
||||||
return self._DEFAULT_PROFILE_PICTURE
|
return self._DEFAULT_PROFILE_PICTURE
|
||||||
|
|
||||||
@property
|
|
||||||
def BASE_URL(self) -> str:
|
|
||||||
return self._config["misc"]["base_url"]
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
|
||||||
|
|
||||||
from beanie import init_beanie
|
from beanie import init_beanie
|
||||||
from pymongo import AsyncMongoClient
|
from pymongo import AsyncMongoClient
|
||||||
from pymongo.asynchronous.collection import AsyncCollection
|
from pymongo.asynchronous.collection import AsyncCollection
|
||||||
from pymongo.errors import ServerSelectionTimeoutError, OperationFailure
|
|
||||||
|
|
||||||
from elm.types import User, Transaction, Ticket, Seat, CateringTypes
|
from elm.types import User, Transaction
|
||||||
from elm.types.ConfigurationTypes import DatabaseConfiguration
|
from elm.types.ConfigurationTypes import DatabaseConfiguration
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
@@ -26,33 +24,14 @@ class DatabaseService:
|
|||||||
self._db_config = db_config
|
self._db_config = db_config
|
||||||
self._client = None
|
self._client = None
|
||||||
self._database = None
|
self._database = None
|
||||||
|
self._users = None
|
||||||
|
|
||||||
async def initialize(self) -> None:
|
async def initialize(self) -> None:
|
||||||
mongo_uri = f"mongodb://{self._db_config.database_user}:{self._db_config.database_password}@{self._db_config.database_host}:{self._db_config.database_port}/{self._db_config.database_name}?authSource=admin"
|
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._client = AsyncMongoClient(mongo_uri)
|
self._client = AsyncMongoClient(self._db_config.database_address)
|
||||||
|
self._database = self._client[self._db_config.database_name]
|
||||||
try:
|
self._users: AsyncCollection = self._database["users"]
|
||||||
await self._client.admin.command("ping")
|
|
||||||
except ServerSelectionTimeoutError:
|
|
||||||
print("Could not connect to mongodb")
|
|
||||||
sys.exit(1)
|
|
||||||
except OperationFailure:
|
|
||||||
print("Authentication with mongodb failed")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
self._database = self._client[
|
|
||||||
self._db_config.database_name
|
|
||||||
]
|
|
||||||
|
|
||||||
await init_beanie(
|
await init_beanie(
|
||||||
database=self._database,
|
database=self._database,
|
||||||
document_models=[
|
document_models=[User, Transaction]
|
||||||
User,
|
|
||||||
Transaction,
|
|
||||||
Ticket,
|
|
||||||
Seat,
|
|
||||||
CateringTypes.CateringMenuItem,
|
|
||||||
CateringTypes.CateringOrder
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
from from_root import from_root
|
|
||||||
|
|
||||||
|
|
||||||
class PreloadService:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._faq: list[tuple[str, str]] = []
|
|
||||||
self._imprint: str = ""
|
|
||||||
self._rules: list[str] = []
|
|
||||||
self._gtc: dict[str, list[str]] = {}
|
|
||||||
|
|
||||||
with open(from_root("src/elm/assets/preloaded_content/faq.txt"), "r") as faq_file:
|
|
||||||
faq = faq_file.read()
|
|
||||||
faq = faq.split("\n\n")
|
|
||||||
for i in range(0, len(faq), 2):
|
|
||||||
q, a = faq[i], faq[i + 1]
|
|
||||||
self._faq.append((q, a))
|
|
||||||
|
|
||||||
with open(from_root("src/elm/assets/preloaded_content/imprint.txt"), "r") as imprint_file:
|
|
||||||
self._imprint = imprint_file.read()
|
|
||||||
|
|
||||||
with open(from_root("src/elm/assets/preloaded_content/rules.txt"), "r") as rules_file:
|
|
||||||
self._rules = rules_file.read().split("\n")
|
|
||||||
|
|
||||||
with open(from_root("src/elm/assets/preloaded_content/gtc.json"), "r") as gtc_file:
|
|
||||||
self._gtc = json.load(gtc_file)
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def faq(self) -> list[tuple[str, str]]:
|
|
||||||
return self._faq
|
|
||||||
|
|
||||||
@property
|
|
||||||
def imprint(self) -> str:
|
|
||||||
return self._imprint
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rules(self) -> list[str]:
|
|
||||||
return self._rules
|
|
||||||
|
|
||||||
@property
|
|
||||||
def gtc(self) -> dict[str, list[str]]:
|
|
||||||
return self._gtc
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import logging
|
|
||||||
from datetime import timezone
|
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from bson import ObjectId
|
|
||||||
|
|
||||||
from elm.types import Seat
|
|
||||||
from elm.types.CateringTypes import CateringOrder
|
|
||||||
from elm.types.ConfigurationTypes import ReceiptPrintingConfiguration
|
|
||||||
from elm.types.User import User
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
|
||||||
logging.getLogger("urllib3").setLevel(logging.FATAL) # Disable logging for urllib3
|
|
||||||
|
|
||||||
class ReceiptPrintingService:
|
|
||||||
def __init__(self, config: ReceiptPrintingConfiguration, dev_mode_enabled: bool) -> None:
|
|
||||||
self._config = config
|
|
||||||
self._dev_mode_enabled = dev_mode_enabled
|
|
||||||
self._url = f"http://{self._config.host}:{self._config.port}/{self._config.order_print_endpoint}"
|
|
||||||
|
|
||||||
async def print_order(self, user: User, order: CateringOrder) -> None:
|
|
||||||
seat = await Seat.find_one({"user.$id": ObjectId(user.id)})
|
|
||||||
if seat is None:
|
|
||||||
seat_id = " - "
|
|
||||||
else:
|
|
||||||
seat_id = str(seat.seat_id)
|
|
||||||
|
|
||||||
|
|
||||||
menu_items_payload = []
|
|
||||||
for item in order.items:
|
|
||||||
menu_items_payload.append({
|
|
||||||
"menu_item_name": item.name,
|
|
||||||
"mods": [f"{'MIT' if mod.selected else 'OHNE'} {mod.label}" for mod in item.selected_modifiers],
|
|
||||||
"amount": 1
|
|
||||||
})
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"order_id": str(order.id)[-5:],
|
|
||||||
"order_date": order.created_at.replace(tzinfo=timezone.utc).astimezone(ZoneInfo("Europe/Berlin")).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z',
|
|
||||||
"customer_name": user.user_name,
|
|
||||||
"seat_id": seat_id,
|
|
||||||
"items": menu_items_payload
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(f"Sending print order to {self._url}: {payload}")
|
|
||||||
try:
|
|
||||||
response = requests.post(
|
|
||||||
self._url,
|
|
||||||
json=payload,
|
|
||||||
headers={"x-password": self._config.password},
|
|
||||||
timeout=2.0
|
|
||||||
)
|
|
||||||
if response.status_code != 200:
|
|
||||||
logger.error(f"Received an error with code {response.status_code}: {response.text}")
|
|
||||||
except Exception as e:
|
|
||||||
if self._dev_mode_enabled:
|
|
||||||
logger.info("An error occurred trying to print a receipt: %s", e)
|
|
||||||
return
|
|
||||||
logger.error("An error occurred trying to print a receipt: %s", e)
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from asyncio import sleep
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from string import ascii_letters, digits
|
from string import ascii_letters, digits
|
||||||
|
|||||||
@@ -4,5 +4,3 @@ from .UserService import UserService, NameNotAllowedError, MailAlreadyInUseError
|
|||||||
from .LocalDataService import LocalData, LocalDataService
|
from .LocalDataService import LocalData, LocalDataService
|
||||||
from .MailingService import MailingService
|
from .MailingService import MailingService
|
||||||
from .AccountingService import AccountingService
|
from .AccountingService import AccountingService
|
||||||
from .PreloadService import PreloadService
|
|
||||||
from .ReceiptPrintingService import ReceiptPrintingService
|
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from datetime import datetime, UTC
|
|
||||||
from decimal import Decimal
|
|
||||||
from enum import StrEnum
|
|
||||||
from typing import Optional, Annotated
|
|
||||||
|
|
||||||
from beanie import Document, PydanticObjectId, Indexed
|
|
||||||
from bson import Decimal128
|
|
||||||
from pydantic import BaseModel, Field, field_validator, ConfigDict
|
|
||||||
|
|
||||||
class CateringMenuItemCategory(StrEnum):
|
|
||||||
MAIN_COURSE = "MAIN_COURSE"
|
|
||||||
DESSERT = "DESSERT"
|
|
||||||
BEVERAGE_NON_ALCOHOLIC = "BEVERAGE_NON_ALCOHOLIC"
|
|
||||||
BEVERAGE_ALCOHOLIC = "BEVERAGE_ALCOHOLIC"
|
|
||||||
BEVERAGE_COCKTAIL = "BEVERAGE_COCKTAIL"
|
|
||||||
BEVERAGE_SHOT = "BEVERAGE_SHOT"
|
|
||||||
BREAKFAST = "BREAKFAST"
|
|
||||||
SNACK = "SNACK"
|
|
||||||
NON_FOOD = "NON_FOOD"
|
|
||||||
|
|
||||||
class CateringOrderStatus(StrEnum):
|
|
||||||
RECEIVED = "RECEIVED"
|
|
||||||
DELAYED = "DELAYED"
|
|
||||||
READY_FOR_PICKUP = "READY_FOR_PICKUP"
|
|
||||||
EN_ROUTE = "EN_ROUTE"
|
|
||||||
COMPLETED = "COMPLETED"
|
|
||||||
CANCELED = "CANCELED"
|
|
||||||
|
|
||||||
class CateringModificationKey(StrEnum):
|
|
||||||
BASE = "base" # For base ingredients that can be deselected, like butter on bread
|
|
||||||
EXTRA = "extra" # For ingredients that can be added, like ketchup on fries
|
|
||||||
|
|
||||||
class MongoDecimalModel(BaseModel):
|
|
||||||
model_config = ConfigDict(
|
|
||||||
arbitrary_types_allowed=True,
|
|
||||||
json_encoders={
|
|
||||||
Decimal: lambda v: str(v)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@field_validator("*", mode="before", check_fields=False)
|
|
||||||
@classmethod
|
|
||||||
def convert_decimal128(cls, v):
|
|
||||||
if isinstance(v, Decimal128):
|
|
||||||
return v.to_decimal()
|
|
||||||
|
|
||||||
return v
|
|
||||||
|
|
||||||
class CateringModifierOption(MongoDecimalModel):
|
|
||||||
key: str
|
|
||||||
label: str
|
|
||||||
|
|
||||||
default_selected: bool = False
|
|
||||||
|
|
||||||
price_delta: Decimal = Decimal("0.00")
|
|
||||||
|
|
||||||
class CateringModifierGroup(MongoDecimalModel):
|
|
||||||
key: CateringModificationKey
|
|
||||||
label: str
|
|
||||||
|
|
||||||
# True = checkbox group
|
|
||||||
# False = radio button group
|
|
||||||
multi_select: bool = True
|
|
||||||
|
|
||||||
min_selected: int = 0
|
|
||||||
max_selected: Optional[int] = None
|
|
||||||
|
|
||||||
options: list[CateringModifierOption] = Field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
class CateringMenuItem(MongoDecimalModel, Document):
|
|
||||||
name: Annotated[str, Indexed(unique=True)]
|
|
||||||
|
|
||||||
category: CateringMenuItemCategory
|
|
||||||
|
|
||||||
description: Optional[str] = None
|
|
||||||
|
|
||||||
base_price: Decimal = Decimal("0.00")
|
|
||||||
|
|
||||||
modifier_groups: list[CateringModifierGroup] = Field(default_factory=list)
|
|
||||||
|
|
||||||
active: bool = True
|
|
||||||
|
|
||||||
created_at: datetime = Field(
|
|
||||||
default_factory=lambda: datetime.now(UTC)
|
|
||||||
)
|
|
||||||
|
|
||||||
updated_at: datetime = Field(
|
|
||||||
default_factory=lambda: datetime.now(UTC)
|
|
||||||
)
|
|
||||||
|
|
||||||
class Settings:
|
|
||||||
name = "catering_menu_items"
|
|
||||||
|
|
||||||
class CateringSelectedModifier(MongoDecimalModel):
|
|
||||||
group_key: str
|
|
||||||
option_key: str
|
|
||||||
|
|
||||||
label: str
|
|
||||||
|
|
||||||
selected: bool
|
|
||||||
|
|
||||||
price_delta: Decimal = Decimal("0.00")
|
|
||||||
|
|
||||||
|
|
||||||
class CateringOrderedItem(MongoDecimalModel):
|
|
||||||
menu_item_id: PydanticObjectId
|
|
||||||
|
|
||||||
# Snapshot fields
|
|
||||||
name: str
|
|
||||||
|
|
||||||
quantity: int = 1
|
|
||||||
|
|
||||||
base_price: Decimal
|
|
||||||
|
|
||||||
selected_modifiers: list[CateringSelectedModifier] = Field(
|
|
||||||
default_factory=list
|
|
||||||
)
|
|
||||||
|
|
||||||
# Final calculated price INCLUDING modifiers
|
|
||||||
final_unit_price: Decimal
|
|
||||||
|
|
||||||
notes: Optional[str] = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_menu_item(cls, menu_item: CateringMenuItem, changed_modifiers: dict[str, bool], quantity: int = 1) -> "CateringOrderedItem":
|
|
||||||
selected_modifiers: list[CateringSelectedModifier] = []
|
|
||||||
|
|
||||||
for modifier_group in menu_item.modifier_groups:
|
|
||||||
for option in modifier_group.options:
|
|
||||||
selected = changed_modifiers.get(option.key)
|
|
||||||
|
|
||||||
if selected is not None:
|
|
||||||
|
|
||||||
selected_modifiers.append(
|
|
||||||
CateringSelectedModifier(
|
|
||||||
group_key=modifier_group.key,
|
|
||||||
option_key=option.key,
|
|
||||||
label=option.label,
|
|
||||||
selected=selected,
|
|
||||||
price_delta=option.price_delta
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
final_unit_price = cls._calculate_final_unit_price(
|
|
||||||
menu_item.base_price,
|
|
||||||
selected_modifiers
|
|
||||||
)
|
|
||||||
|
|
||||||
return cls(
|
|
||||||
menu_item_id=menu_item.id,
|
|
||||||
name=menu_item.name,
|
|
||||||
quantity=quantity,
|
|
||||||
base_price=menu_item.base_price,
|
|
||||||
selected_modifiers=selected_modifiers,
|
|
||||||
final_unit_price=final_unit_price
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _calculate_final_unit_price(base_price: Decimal, modifiers: list[CateringSelectedModifier]) -> Decimal:
|
|
||||||
|
|
||||||
return (
|
|
||||||
base_price +
|
|
||||||
sum(
|
|
||||||
modifier.price_delta
|
|
||||||
for modifier in modifiers
|
|
||||||
if modifier.selected
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
class CateringOrder(Document):
|
|
||||||
customer_id: PydanticObjectId
|
|
||||||
|
|
||||||
items: list[CateringOrderedItem] = Field(default_factory=list)
|
|
||||||
|
|
||||||
status: CateringOrderStatus = CateringOrderStatus.RECEIVED
|
|
||||||
|
|
||||||
created_at: datetime = Field(
|
|
||||||
default_factory=lambda: datetime.now(UTC)
|
|
||||||
)
|
|
||||||
|
|
||||||
class Settings:
|
|
||||||
name = "catering_orders"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def translate_order_status(status: CateringOrderStatus) -> str:
|
|
||||||
if status == CateringOrderStatus.RECEIVED:
|
|
||||||
return "Eingegangen"
|
|
||||||
elif status == CateringOrderStatus.DELAYED:
|
|
||||||
return "Verzögert"
|
|
||||||
elif status == CateringOrderStatus.READY_FOR_PICKUP:
|
|
||||||
return "Abholbereit"
|
|
||||||
elif status == CateringOrderStatus.EN_ROUTE:
|
|
||||||
return "In Zustellung"
|
|
||||||
elif status == CateringOrderStatus.COMPLETED:
|
|
||||||
return "Abgeschlossen"
|
|
||||||
elif status == CateringOrderStatus.CANCELED:
|
|
||||||
return "Storniert"
|
|
||||||
else:
|
|
||||||
raise RuntimeError("Unknown CateringOrderStatus:", status)
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
|
|
||||||
class NoSuchCategoryError(Exception):
|
class NoSuchCategoryError(Exception):
|
||||||
pass
|
pass
|
||||||
@@ -16,11 +14,8 @@ class MailingServiceConfiguration:
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class DatabaseConfiguration:
|
class DatabaseConfiguration:
|
||||||
database_host: str
|
database_address: str
|
||||||
database_port: str
|
|
||||||
database_name: str
|
database_name: str
|
||||||
database_user: str
|
|
||||||
database_password :str
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class LanInfo:
|
class LanInfo:
|
||||||
@@ -34,8 +29,6 @@ class LanInfo:
|
|||||||
has_showers: bool
|
has_showers: bool
|
||||||
ts3_address: str
|
ts3_address: str
|
||||||
discord_invite_link: str
|
discord_invite_link: str
|
||||||
location_name: str
|
|
||||||
location_link: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -51,12 +44,3 @@ class PayPalConfiguration:
|
|||||||
secret_sandbox: str
|
secret_sandbox: str
|
||||||
client_id: str
|
client_id: str
|
||||||
secret: str
|
secret: str
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class TicketInfo:
|
|
||||||
category: str
|
|
||||||
total_tickets: int
|
|
||||||
price: Decimal
|
|
||||||
description: str
|
|
||||||
additional_info: str
|
|
||||||
can_be_sold: bool
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from beanie import Document, Link
|
|
||||||
|
|
||||||
from elm.types import User
|
|
||||||
|
|
||||||
|
|
||||||
class Seat(Document):
|
|
||||||
seat_id: str
|
|
||||||
is_blocked: bool
|
|
||||||
category: str
|
|
||||||
user: Optional[Link[User]] = None
|
|
||||||
|
|
||||||
class Settings:
|
|
||||||
name = "seats"
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
from enum import Enum
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from beanie import Document, Link
|
|
||||||
|
|
||||||
from elm.types import User
|
|
||||||
|
|
||||||
class TicketState(Enum):
|
|
||||||
AVAILABLE = 1
|
|
||||||
SOLD_OUT = 2
|
|
||||||
UNAVAILABLE = 3
|
|
||||||
|
|
||||||
|
|
||||||
class Ticket(Document):
|
|
||||||
category: str
|
|
||||||
purchase_date: datetime
|
|
||||||
owner: Optional[Link[User]] = None
|
|
||||||
|
|
||||||
class Settings:
|
|
||||||
name = "tickets"
|
|
||||||
@@ -4,7 +4,7 @@ from typing import Annotated
|
|||||||
|
|
||||||
from beanie import Document, Indexed
|
from beanie import Document, Indexed
|
||||||
from bson import Decimal128
|
from bson import Decimal128
|
||||||
from pydantic import field_validator, Field
|
from pydantic import field_validator
|
||||||
|
|
||||||
|
|
||||||
class Transaction(Document):
|
class Transaction(Document):
|
||||||
@@ -12,9 +12,7 @@ class Transaction(Document):
|
|||||||
value: Decimal
|
value: Decimal
|
||||||
is_debit: bool
|
is_debit: bool
|
||||||
title: str
|
title: str
|
||||||
transaction_date: datetime = Field(
|
transaction_date: datetime = datetime.now(UTC)
|
||||||
default_factory=lambda: datetime.now(UTC)
|
|
||||||
)
|
|
||||||
|
|
||||||
class Settings:
|
class Settings:
|
||||||
name = "transactions"
|
name = "transactions"
|
||||||
@@ -25,4 +23,3 @@ class Transaction(Document):
|
|||||||
if isinstance(v, Decimal128):
|
if isinstance(v, Decimal128):
|
||||||
return v.to_decimal()
|
return v.to_decimal()
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
from datetime import date, datetime, UTC
|
from datetime import date, datetime
|
||||||
from typing import Optional, Annotated
|
from typing import Optional, Annotated
|
||||||
|
|
||||||
from beanie import Document, Indexed
|
from beanie import Document, Indexed
|
||||||
from pydantic import Field
|
|
||||||
|
|
||||||
|
|
||||||
class User(Document):
|
class User(Document):
|
||||||
@@ -16,9 +15,7 @@ class User(Document):
|
|||||||
user_birth_day: Optional[date] = None
|
user_birth_day: Optional[date] = None
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
is_team_member: bool = False
|
is_team_member: bool = False
|
||||||
created_at: datetime = Field(
|
created_at: datetime = datetime.now()
|
||||||
default_factory=lambda: datetime.now(UTC)
|
|
||||||
)
|
|
||||||
|
|
||||||
class Settings:
|
class Settings:
|
||||||
name = "users"
|
name = "users"
|
||||||
|
|||||||
@@ -2,6 +2,3 @@ from .User import User
|
|||||||
from .UserSession import UserSession
|
from .UserSession import UserSession
|
||||||
from .ConfigurationTypes import *
|
from .ConfigurationTypes import *
|
||||||
from .Transaction import Transaction
|
from .Transaction import Transaction
|
||||||
from .Ticket import Ticket, TicketState
|
|
||||||
from .Seat import Seat
|
|
||||||
from .ConfigurationTypes import *
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
[app]
|
|
||||||
app-type = "website"
|
|
||||||
main-module = "elm"
|
|
||||||
project-files = ["*.py", "/assets/", "/rio.toml"]
|
|
||||||
Reference in New Issue
Block a user