prerelease/0.6.0 #1

Merged
Typhus merged 29 commits from prerelease/0.6.0 into main 2026-05-27 23:17:52 +00:00
11 changed files with 182 additions and 76 deletions
Showing only changes of commit 63b64bbed1 - Show all commits
-3
View File
@@ -1,3 +0,0 @@
# ELM
The EZGG LAN Manager
+2 -2
View File
@@ -27,14 +27,14 @@
price="20.00"
description="Normales Ticket"
additional_info="Berechtigt zur Nutzung eines regulären Platzes für die gesamte Dauer der LAN"
is_default=true
can_be_sold=true
[tickets."DELUXE"]
total_tickets=30
price="25.00"
description="Deluxe Ticket"
additional_info="Wie das normale Ticket, aber mit doppelt so breitem Tisch (160cm)"
is_default=false
can_be_sold=true
[receipt_printing]
host="10.0.0.103"
+16 -6
View File
@@ -1,7 +1,9 @@
from typing import Optional
from rio import Component, Rectangle, Column, Text, Row, PointerEventListener, TextInput
from rio.event import on_populate
from elm.types import UserSession
from elm.types import UserSession, Ticket
from elm.components import ElmButton
from elm.services import UserService
@@ -13,14 +15,19 @@ class AccountInfoBox(Component):
account_info_is_error: bool = False
password_input_blocked: bool = False
password_change_in_progress: bool = False
ticket: Optional[Ticket] = None
@on_populate
async def on_populate(self) -> None:
try:
user = await self.session[UserService].get_user(self.session[UserSession].user_name)
if user:
self.mail = user.user_mail
self.ticket = await Ticket.find_one({"owner.$id": user.id})
else:
self.session.navigate_to("./login")
except KeyError:
self.session.navigate_to("./login")
async def set_new_password(self) -> None:
self.password_change_in_progress = True
@@ -55,6 +62,9 @@ class AccountInfoBox(Component):
def build(self) -> Component:
row_col = Row
ticket_text = "-"
if self.ticket:
ticket_text = self.ticket.category
if self.session.is_mobile():
row_col = Column
@@ -75,13 +85,13 @@ class AccountInfoBox(Component):
row_col(
PointerEventListener(
Rectangle(
content=Row(Text("Ticket:", margin=1, overflow="wrap", justify="left"), Text("-", margin=1, overflow="wrap", justify="right")),
fill=self.session.theme.danger_color_dark,
content=Row(Text("Ticket:", margin=1, overflow="wrap", justify="left"), Text(ticket_text, margin=1, overflow="wrap", justify="right")),
fill=self.session.theme.success_color if self.ticket else self.session.theme.danger_color_dark,
stroke_width=0.1,
stroke_color=self.session.theme.danger_color,
hover_fill=self.session.theme.danger_color,
stroke_color=self.session.theme.success_color if self.ticket else self.session.theme.danger_color,
hover_fill=self.session.theme.success_color if self.ticket else self.session.theme.danger_color,
hover_stroke_width=0.1,
hover_stroke_color=self.session.theme.danger_color_dark,
hover_stroke_color=self.session.theme.success_color if self.ticket else self.session.theme.danger_color_dark,
transition_time=0.2,
cursor="pointer"
),
+137
View File
@@ -0,0 +1,137 @@
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()
)
+6 -3
View File
@@ -17,8 +17,11 @@ class ElmButton(Component):
style: Literal["small", "normal"] = "normal"
wrap: bool = False
is_loading: bool = False
is_disabled: bool = False
async def _on_press(self, event: PointerEvent) -> None:
if self.is_disabled:
return
if iscoroutinefunction(self.on_press):
await self.on_press()
else:
@@ -54,10 +57,10 @@ class ElmButton(Component):
stroke_width=0.1,
stroke_color=self.session.theme.secondary_color,
hover_stroke_width=0.1,
hover_stroke_color=self.session.theme.hud_color,
hover_fill=self.session.theme.hud_color,
hover_stroke_color=self.session.theme.secondary_color if self.is_disabled else self.session.theme.hud_color,
hover_fill=Color.TRANSPARENT if self.is_disabled else self.session.theme.hud_color,
transition_time=0,
cursor="pointer"
cursor="not-allowed" if self.is_disabled else "pointer"
),
on_press=self._on_press
)
+1
View File
@@ -6,3 +6,4 @@ from .ElmButton import ElmButton
from .AvatarEditBox import AvatarEditBox
from .AccountInfoBox import AccountInfoBox
from .PersonalInfoBox import PersonalInfoBox
from .BuyTicketBox import BuyTicketBox
+4 -54
View File
@@ -1,67 +1,17 @@
from __future__ import annotations
from rio import Component, Column, Row, Text, Spacer, page, Color, TextStyle, Rectangle, TextInput, ProgressBar, Dict
from rio.event import on_populate
from elm.services import ConfigurationService, AccountingService
from elm.components import LanCountdownBox, LanInfoBox, LandingPageBoxFull, LandingPageBoxHalf, ElmButton
from elm.types import Ticket
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):
sold_tickets_by_category: Dict[str, int] = Dict()
"""
ToDo: Implement conditional ticket buying (check login!)
"""
@on_populate
async def on_populate(self) -> None:
for ticket_info in self.session[ConfigurationService].get_ticket_info():
self.sold_tickets_by_category[ticket_info.category] = len(await Ticket.find_many(Ticket.category == ticket_info.category).to_list())
def get_available_tickets_by_category(self, category: str, total_tickets: int) -> int:
return total_tickets - self.sold_tickets_by_category.get(category, 0)
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(
Column(
Rectangle(
content=Column(
Rectangle(
content=Rectangle(
content=Row(
Text(ticket_info.description, margin=0.5, selectable=False, overflow="wrap", grow_x=True),
Text(self.session[AccountingService].make_euro_string_from_decimal(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(ticket_info.additional_info, overflow="wrap", margin_bottom=1),
Text("Du besitzt dieses Ticket!", 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_by_category(ticket_info.category, ticket_info.total_tickets)} / {ticket_info.total_tickets}", justify="right", font_size=0.8 if self.session.is_mobile() else 1)),
ProgressBar(progress=self.get_available_tickets_by_category(ticket_info.category, ticket_info.total_tickets) / ticket_info.total_tickets, min_height=1),
ElmButton(text="Kaufen"),
margin=1,
spacing=1
),
Spacer()
),
fill=self.session.theme.box_color,
stroke_width=0.1,
stroke_color=self.session.theme.box_border_color
),
Spacer()
)
)
ticket_boxes.append(BuyTicketBox(ticket_info=ticket_info))
return row_col(
*ticket_boxes,
+2 -1
View File
@@ -54,7 +54,8 @@ class ConfigurationService:
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"]
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)
+1
View File
@@ -54,3 +54,4 @@ class TicketInfo:
price: Decimal
description: str
additional_info: str
can_be_sold: bool
+6
View File
@@ -1,10 +1,16 @@
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
+1 -1
View File
@@ -2,4 +2,4 @@ from .User import User
from .UserSession import UserSession
from .ConfigurationTypes import *
from .Transaction import Transaction
from .Ticket import Ticket
from .Ticket import Ticket, TicketState