9 Commits

Author SHA1 Message Date
David Rodenkirchen 6217c1b2d0 add lan info location 2026-05-30 23:13:59 +02:00
David Rodenkirchen 33d937d385 add lan info location 2026-05-30 23:12:39 +02:00
David Rodenkirchen 210e01bb96 Add option to storno tickets and seats 2026-05-30 20:28:26 +02:00
David Rodenkirchen 041ddaa334 Add participants page 2026-05-30 09:43:50 +00:00
David Rodenkirchen bbcf18d790 bump version 2026-05-30 09:43:50 +00:00
root 87eb94045c Add Python 3.11 compatibility 2026-05-29 23:17:28 +02:00
David Rodenkirchen 36418470a6 make login more sturdy 2026-05-28 13:16:00 +02:00
David Rodenkirchen 11724ad0d9 improve error handling 2026-05-28 13:10:01 +02:00
David Rodenkirchen edeefe072d Cleanup requirements 2026-05-28 13:08:48 +02:00
16 changed files with 177 additions and 18 deletions
+1 -1
View File
@@ -1 +1 @@
0.6.0-prerelease 0.6.1
+2
View File
@@ -9,6 +9,8 @@
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" database_host="localhost"
BIN
View File
Binary file not shown.
+3 -1
View File
@@ -1,4 +1,4 @@
from rio import Component, Rectangle, Row, Text, Spacer, ProgressBar, Column, Color, TextStyle from rio import Component, Rectangle, Row, Text, Spacer, ProgressBar, Column, Color, TextStyle, Link
from rio.event import on_populate from rio.event import on_populate
from elm.services import ConfigurationService from elm.services import ConfigurationService
@@ -36,6 +36,7 @@ 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")),
@@ -81,6 +82,7 @@ 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")),
+1
View File
@@ -94,6 +94,7 @@ 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
+2 -1
View File
@@ -20,7 +20,8 @@ class SeatPixel(Component):
self.associated_user = await self.seat.user.fetch() self.associated_user = await self.seat.user.fetch()
async def on_press(self, _: PointerEvent) -> None: async def on_press(self, _: PointerEvent) -> None:
self.session.navigate_to(f"./seat-info?seat_id={self.seat_id.replace("\n", "")}") seat_id = self.seat_id.replace("\n", "")
self.session.navigate_to(f"./seat-info?seat_id={seat_id}")
def determine_color(self) -> Color: def determine_color(self) -> Color:
if self.seat is not None: if self.seat is not None:
+1 -1
View File
@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from _sha2 import sha256 from hashlib import sha256
from random import choices from random import choices
from typing import Any, Optional from typing import Any, Optional
+3
View File
@@ -32,6 +32,9 @@ 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 -1
View File
@@ -26,7 +26,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,
+66
View File
@@ -0,0 +1,66 @@
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
)
+73 -2
View File
@@ -1,15 +1,18 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from copy import copy
from functools import partial from functools import partial
from typing import Optional from typing import Optional
from decimal import Decimal 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 import Component, Column, Row, Text, Spacer, page, Rectangle, TextInput, GuardEvent, Button, TextInputChangeEvent, NumberInput, IconButton
from rio.event import on_populate from rio.event import on_populate
from elm.types import UserSession, User, Transaction from elm.types import UserSession, User, Transaction, Ticket, Seat
from elm.services import AccountingService, MailingService from elm.services import AccountingService, MailingService, ConfigurationService
from elm.components import AccountInfoBox from elm.components import AccountInfoBox
logger = logging.getLogger(__name__.split(".")[-1]) logger = logging.getLogger(__name__.split(".")[-1])
@@ -95,6 +98,36 @@ class UserAdminPage(Component):
receiver=self.active_user.user_mail 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: def build(self) -> Component:
right_panel_contents = [] right_panel_contents = []
if not self.active_user: if not self.active_user:
@@ -153,6 +186,44 @@ class UserAdminPage(Component):
stroke_width=0.1, stroke_width=0.1,
stroke_color=self.session.theme.box_border_color 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
),
]) ])
+7 -6
View File
@@ -130,7 +130,7 @@ class AccountingService:
return True return True
return False return False
async def add_balance(self, user_name: str, balance_to_add: Decimal, title: str) -> Decimal: async def add_balance(self, user_name: str, balance_to_add: Decimal, title: str, skip_mail: bool = False) -> 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")
@@ -142,11 +142,12 @@ class AccountingService:
).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) new_balance = await self.get_balance(user_name)
await self._mailing_service.send_email( if not skip_mail:
"Dein Guthaben wurde aufgeladen", await self._mailing_service.send_email(
self._mailing_service.generate_account_balance_added_mail_body(user, balance_to_add, new_balance), "Dein Guthaben wurde aufgeladen",
user.user_mail self._mailing_service.generate_account_balance_added_mail_body(user, balance_to_add, new_balance),
) user.user_mail
)
return new_balance 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:
+3 -1
View File
@@ -103,7 +103,9 @@ 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...")
+11
View File
@@ -1,8 +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, Ticket, Seat, CateringTypes
from elm.types.ConfigurationTypes import DatabaseConfiguration from elm.types.ConfigurationTypes import DatabaseConfiguration
@@ -30,6 +32,15 @@ class DatabaseService:
if self._client is None: if self._client is None:
self._client = AsyncMongoClient(mongo_uri) self._client = AsyncMongoClient(mongo_uri)
try:
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._database = self._client[
self._db_config.database_name self._db_config.database_name
] ]
-3
View File
@@ -1,4 +1,3 @@
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
@@ -69,8 +68,6 @@ class UserService:
async def is_login_valid(self, user_name: str, password_clear_text: str) -> bool: async def is_login_valid(self, user_name: str, password_clear_text: str) -> bool:
user = await self.get_user(user_name) user = await self.get_user(user_name)
if not user:
user = await self.get_user(user_name.lower()) # Migrated users had all lowercase names
user_password_hash = sha256(password_clear_text.encode(encoding="utf-8")).hexdigest() user_password_hash = sha256(password_clear_text.encode(encoding="utf-8")).hexdigest()
if not user: if not user:
return False return False
+2
View File
@@ -34,6 +34,8 @@ 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)