rename lan

This commit was merged in pull request #22.
This commit is contained in:
David Rodenkirchen
2025-07-26 14:16:09 +02:00
parent 6e598b577f
commit 29caadaca2
81 changed files with 234 additions and 234 deletions
+241
View File
@@ -0,0 +1,241 @@
from decimal import Decimal
from functools import partial
from typing import Optional
from rio import Column, Component, event, Text, TextStyle, Button, Color, Revealer, Row, ProgressCircle, Link
from src.ezgg_lan_manager import ConfigurationService, UserService, AccountingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
from src.ezgg_lan_manager.types.Transaction import Transaction
from src.ezgg_lan_manager.types.User import User
class AccountPage(Component):
user: Optional[User] = None
balance: Optional[Decimal] = None
transaction_history: list[Transaction] = list()
banking_info_revealer_open: bool = False
paypal_info_revealer_open: bool = False
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Guthabenkonto")
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
self.balance = await self.session[AccountingService].get_balance(self.user.user_id)
self.transaction_history = await self.session[AccountingService].get_transaction_history(self.user.user_id)
async def _on_banking_info_press(self) -> None:
self.banking_info_revealer_open = not self.banking_info_revealer_open
async def _on_paypal_info_press(self) -> None:
self.paypal_info_revealer_open = not self.paypal_info_revealer_open
def build(self) -> Component:
if not self.user and not self.balance:
return Column(
MainViewContentBox(
ProgressCircle(
color="secondary",
align_x=0.5,
margin_top=2,
margin_bottom=2
)
),
align_y=0,
)
banking_info_revealer = Revealer(
is_open=self.bind().banking_info_revealer_open,
header=None,
content=Column(
Text(
"Bankverbindung:",
style=TextStyle(
fill=self.session.theme.background_color
),
margin=0,
margin_top=0,
margin_bottom=1,
align_x=0.5
),
Text(
"Kontoinhaber: Einfach Zocken Gaming Gesellschaft\n"
"IBAN: DE47 5176 2434 0019 8566 07\n"
"BLZ: 51762434\n"
"BIC: GENODE51BIK\n\n"
"Verwendungszweck:",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.7
),
margin=0,
margin_bottom=1,
align_x=0.2
),
Text(
f"AUFLADUNG - {self.user.user_id} - {self.user.user_name}",
style=TextStyle(
fill=self.session.theme.neutral_color
),
margin=0,
margin_bottom=1,
align_x=0.5
)
),
margin=2,
margin_top=0,
margin_bottom=1,
grow_x=True
)
paypal_info_revealer = Revealer(
is_open=self.bind().paypal_info_revealer_open,
header=None,
content=Column(
Text(
"PayPal Verbindung:",
style=TextStyle(
fill=self.session.theme.background_color
),
margin=0,
margin_top=0,
margin_bottom=1,
align_x=0.5
),
Text(
"Empfänger: tech@ezgg-ev.de\n"
"Zahlungsart: Freunde und Familie\n"
"Verwendungszweck:",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.7
),
margin=0,
margin_bottom=1,
align_x=0.2
),
Text(
f"AUFLADUNG - {self.user.user_id} - {self.user.user_name}",
style=TextStyle(
fill=self.session.theme.neutral_color
),
margin=0,
margin_bottom=1,
align_x=0.5
)
),
margin=2,
margin_top=0,
margin_bottom=1,
grow_x=True
)
transaction_history = Column(
Text(
"Transaktionshistorie",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=1,
margin_bottom=1,
align_x=0.5
)
)
for transaction in sorted(self.transaction_history, key=lambda t: t.transaction_date, reverse=True):
transaction_history.add(
Row(
Text(
f"{transaction.reference} ({transaction.transaction_date.strftime('%d.%m - %H:%M')})",
style=TextStyle(
fill=self.session.theme.danger_color if transaction.is_debit else self.session.theme.success_color,
font_size=0.8
),
margin=0,
margin_top=0,
margin_left=0.5,
margin_bottom=0.4,
align_x=0
),
Text(
f"{'-' if transaction.is_debit else '+'}{AccountingService.make_euro_string_from_decimal(transaction.value)}",
style=TextStyle(
fill=self.session.theme.danger_color if transaction.is_debit else self.session.theme.success_color,
font_size=0.8
),
margin=0,
margin_top=0,
margin_right=0.5,
margin_bottom=0.4,
align_x=1
)
)
)
return Column(
MainViewContentBox(
content=Text(
f"Kontostand: {AccountingService.make_euro_string_from_decimal(self.balance)}",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=2,
align_x=0.5
)
),
MainViewContentBox(
content=Column(
Text(
"LAN-Konto aufladen",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=2,
align_x=0.5
),
Button(
content=Text("BANKÜBERWEISUNG", style=TextStyle(fill=Color.from_hex("121212"), font_size=0.8), justify="center"),
shape="rectangle",
style="major",
color="secondary",
grow_x=True,
margin=2,
margin_top=0,
margin_bottom=1,
on_press=self._on_banking_info_press
),
banking_info_revealer,
Button(
content=Text("PAYPAL (ohne Gebühr - Freunde&Familie)", style=TextStyle(fill=Color.from_hex("121212"), font_size=0.8), justify="center"),
shape="rectangle",
style="major",
color="secondary",
grow_x=True,
margin=2,
margin_top=0,
on_press=self._on_paypal_info_press
),
paypal_info_revealer,
Link(
content=Button(
content=Text("PAYPAL (3% Gebühr - Gewerblich)", style=TextStyle(fill=Color.from_hex("121212"), font_size=0.8), justify="center"),
shape="rectangle",
style="major",
color="secondary",
grow_x=True,
margin=2,
margin_top=0
),
target_url="https://www.paypal.com/ncp/payment/89YMGVZ4S33RS",
open_in_new_tab=True
)
)
),
MainViewContentBox(
content=transaction_history
),
align_y=0,
)
+97
View File
@@ -0,0 +1,97 @@
from __future__ import annotations
from typing import * # type: ignore
from rio import Component, event, Spacer, Card, Container, Column, Row, TextStyle, Color, Text, PageView, Button
from src.ezgg_lan_manager import ConfigurationService, DatabaseService
from src.ezgg_lan_manager.components.DesktopNavigation import DesktopNavigation
class BasePage(Component):
color = "secondary"
corner_radius = (0, 0.5, 0, 0)
footer_size = 53.1
force_portrait_mode = False
@event.periodic(60)
async def check_db_conn(self) -> None:
is_healthy = await self.session[DatabaseService].is_healthy()
if not is_healthy:
self.session.navigate_to("./db-error")
@event.on_window_size_change
async def on_window_size_change(self):
self.force_refresh()
@event.on_page_change
def check_needed_size(self):
# ToDo: Low-Prio: Change layout, so the footer is always as wide as needed.
# This is a workaround, bc the seating page needs more width
if "/seating" in self.session.active_page_url.__str__():
self.footer_size = 78.2
else:
self.footer_size = 53.1
self.force_refresh()
def enforce_portrait_mode(self) -> None:
self.force_portrait_mode = True
self.force_refresh()
def build(self) -> Component:
content = Card(
PageView(),
color="secondary",
min_width=38,
corner_radius=(0, 0.5, 0, 0)
)
if self.session.window_width > 28 or self.force_portrait_mode:
return Container(
content=Column(
Column(
Row(
Spacer(grow_x=True, grow_y=True),
DesktopNavigation(),
content,
Spacer(grow_x=True, grow_y=True),
grow_y=True
),
Row(
Spacer(grow_x=True, grow_y=False),
Card(
content=Text(f"EZGG LAN Manager Version {self.session[ConfigurationService].APP_VERSION} © EZ GG e.V.", align_x=0.5, align_y=0.5, fill=self.session.theme.primary_color, style=TextStyle(font_size=0.5)),
color=self.session.theme.neutral_color,
corner_radius=(0, 0, 0.5, 0.5),
grow_x=False,
grow_y=False,
min_height=1.2,
min_width=self.footer_size,
margin_bottom=3
),
Spacer(grow_x=True, grow_y=False),
grow_y=False
),
margin_top=4
)
),
grow_x=True,
grow_y=True
)
else:
return Column(
Text(
"Wir empfehlen auf\nmobilen Endgeräten im\nQuerformat zu arbeiten.\n\nBitte drehe dein Gerät.",
fill=Color.from_hex("FFFFFF"),
align_x=0.5,
align_y=0.5,
style=TextStyle(font_size=0.8)
),
Button(
content=Text("Ohne drehen fortfahren", margin=0.2),
style="minor",
shape="rounded",
align_x=0.5,
align_y=0,
on_press=self.enforce_portrait_mode
)
)
+118
View File
@@ -0,0 +1,118 @@
from typing import Optional
from rio import Text, Column, TextStyle, Component, event, Button, Popup
from src.ezgg_lan_manager import ConfigurationService, UserService, TicketingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.TicketBuyCard import TicketBuyCard
from src.ezgg_lan_manager.services.AccountingService import InsufficientFundsError
from src.ezgg_lan_manager.services.TicketingService import TicketNotAvailableError, UserAlreadyHasTicketError
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
from src.ezgg_lan_manager.types.Ticket import Ticket
from src.ezgg_lan_manager.types.User import User
class BuyTicketPage(Component):
user: Optional[User] = None
user_ticket: Optional[Ticket] = None
is_popup_open: bool = False
popup_message: str = ""
is_popup_success: bool = False
is_buying_enabled: bool = False
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Ticket kaufen")
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
if self.user is None: # No user logged in
self.is_buying_enabled = False
else: # User is logged in
possible_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id)
self.user_ticket = possible_ticket
if possible_ticket is not None: # User already has a ticket
self.is_buying_enabled = False
else:
self.is_buying_enabled = True
async def on_buy_pressed(self, category: str) -> None:
if not self.user:
return
self.is_buying_enabled = False
self.force_refresh()
try:
t_s = self.session[TicketingService]
ticket = await t_s.purchase_ticket(self.user.user_id, category)
self.popup_message = f"Ticket erfolgreich gekauft. Deine Ticket-ID lautet: {ticket.ticket_id}."
self.is_popup_success = True
except TicketNotAvailableError:
self.popup_message = "Das ausgewählte Ticket ist nicht verfügbar."
self.is_popup_success = False
except InsufficientFundsError:
self.popup_message = "Dein Guthaben reicht nicht aus um dieses Ticket zu kaufen."
self.is_popup_success = False
except UserAlreadyHasTicketError:
self.popup_message = (f"Du besitzt bereits ein Ticket. Um dein aktuelles Ticket zu stornieren, kontaktiere bitte den Support unter "
f"{self.session[ConfigurationService].get_lan_info().organizer_mail}.")
self.is_popup_success = False
except RuntimeError:
self.popup_message = "Ein unbekannter Fehler ist aufgetreten."
self.is_popup_success = False
self.is_popup_open = True
await self.on_populate()
async def on_popup_close_pressed(self) -> None:
self.is_popup_open = False
self.popup_message = ""
def build(self) -> Component:
ticket_infos = self.session[ConfigurationService].get_ticket_info()
header = Text(
"Tickets & Preise",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
)
return Column(
MainViewContentBox(
Column(
header,
Popup(
anchor=header,
content=Column(
Text(
self.popup_message,
style=TextStyle(font_size=1.1, fill=self.session.theme.success_color if self.is_popup_success else self.session.theme.danger_color),
overflow="wrap",
grow_y=True,
margin=1
),
Button("Bestätigen", shape="rounded", grow_y=False, on_press=self.on_popup_close_pressed),
min_width=34,
min_height=10
),
is_open=self.is_popup_open,
position="bottom",
margin=1,
corner_radius=0.2,
color=self.session.theme.primary_color
),
*[TicketBuyCard(
description=t.description,
additional_info=t.additional_info,
price=t.price,
category=t.category,
pressed_cb=self.on_buy_pressed,
is_enabled=self.is_buying_enabled,
total_tickets=t.total_tickets,
user_ticket=self.user_ticket
) for t in ticket_infos]
),
),
align_y=0
)
+284
View File
@@ -0,0 +1,284 @@
from typing import Optional, Callable
from rio import Column, Component, event, TextStyle, Text, Spacer, Revealer, SwitcherBar, SwitcherBarChangeEvent, ProgressCircle
from src.ezgg_lan_manager import ConfigurationService, CateringService
from src.ezgg_lan_manager.components.CateringSelectionItem import CateringSelectionItem
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.ShoppingCartAndOrders import ShoppingCartAndOrders
from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItemCategory, CateringMenuItem
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
class CateringPage(Component):
show_cart = True
all_menu_items: Optional[list[CateringMenuItem]] = None
shopping_cart_and_orders: list[ShoppingCartAndOrders] = []
def __post_init__(self) -> None:
self.session[SessionStorage].subscribe_to_logged_in_or_out_event(self.__class__.__name__, self.on_user_logged_in_status_changed)
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Catering")
self.all_menu_items = await self.session[CateringService].get_menu()
async def on_user_logged_in_status_changed(self) -> None:
self.force_refresh()
async def on_switcher_bar_changed(self, _: SwitcherBarChangeEvent) -> None:
await self.shopping_cart_and_orders[0].switch()
@staticmethod
def get_menu_items_by_category(all_menu_items: list[CateringMenuItem], category: Optional[CateringMenuItemCategory]) -> list[CateringMenuItem]:
return list(filter(lambda item: item.category == category, all_menu_items))
def build(self) -> Component:
user_id = self.session[SessionStorage].user_id
if len(self.shopping_cart_and_orders) == 0:
self.shopping_cart_and_orders.append(ShoppingCartAndOrders())
if len(self.shopping_cart_and_orders) > 1:
self.shopping_cart_and_orders.clear()
self.shopping_cart_and_orders.append(ShoppingCartAndOrders())
switcher_bar = SwitcherBar(
values=["cart", "orders"],
names=["Warenkorb", "Bestellungen"],
selected_value="cart",
margin_left=5,
margin_right=5,
margin_top=1,
margin_bottom=1,
color=self.session.theme.hud_color,
on_change=self.on_switcher_bar_changed
)
shopping_cart_and_orders_container = MainViewContentBox(
Column(
Text(
text="Catering",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=0,
align_x=0.5
),
switcher_bar,
self.shopping_cart_and_orders[0]
)
) if user_id else Spacer()
menu = [MainViewContentBox(
ProgressCircle(
color="secondary",
align_x=0.5,
margin_top=2,
margin_bottom=2
)
)] if not self.all_menu_items else [MainViewContentBox(
Revealer(
header="Snacks",
content=Column(
*[CateringSelectionItem(
article_name=catering_menu_item.name,
article_price=catering_menu_item.price,
article_id=catering_menu_item.item_id,
on_add_callback=self.shopping_cart_and_orders[0].on_add_item,
is_sensitive=(user_id is not None) and not catering_menu_item.is_disabled,
additional_info=catering_menu_item.additional_info,
is_grey=idx % 2 == 0
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.SNACK))],
),
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=1,
align_y=0.5
)
),
MainViewContentBox(
Revealer(
header="Frühstück",
content=Column(
*[CateringSelectionItem(
article_name=catering_menu_item.name,
article_price=catering_menu_item.price,
article_id=catering_menu_item.item_id,
on_add_callback=self.shopping_cart_and_orders[0].on_add_item,
is_sensitive=(user_id is not None) and not catering_menu_item.is_disabled,
additional_info=catering_menu_item.additional_info,
is_grey=idx % 2 == 0
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.BREAKFAST))],
),
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=1,
align_y=0.5
)
),
MainViewContentBox(
Revealer(
header="Hauptspeisen",
content=Column(
*[CateringSelectionItem(
article_name=catering_menu_item.name,
article_price=catering_menu_item.price,
article_id=catering_menu_item.item_id,
on_add_callback=self.shopping_cart_and_orders[0].on_add_item,
is_sensitive=(user_id is not None) and not catering_menu_item.is_disabled,
additional_info=catering_menu_item.additional_info,
is_grey=idx % 2 == 0
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.MAIN_COURSE))],
),
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=1,
align_y=0.5
)
),
MainViewContentBox(
Revealer(
header="Desserts",
content=Column(
*[CateringSelectionItem(
article_name=catering_menu_item.name,
article_price=catering_menu_item.price,
article_id=catering_menu_item.item_id,
on_add_callback=self.shopping_cart_and_orders[0].on_add_item,
is_sensitive=(user_id is not None) and not catering_menu_item.is_disabled,
additional_info=catering_menu_item.additional_info,
is_grey=idx % 2 == 0
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.DESSERT))],
),
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=1,
align_y=0.5
)
),
MainViewContentBox(
Revealer(
header="Wasser & Softdrinks",
content=Column(
*[CateringSelectionItem(
article_name=catering_menu_item.name,
article_price=catering_menu_item.price,
article_id=catering_menu_item.item_id,
on_add_callback=self.shopping_cart_and_orders[0].on_add_item,
is_sensitive=(user_id is not None) and not catering_menu_item.is_disabled,
additional_info=catering_menu_item.additional_info,
is_grey=idx % 2 == 0
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC))],
),
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=1,
align_y=0.5
)
),
MainViewContentBox(
Revealer(
header="Alkoholische Getränke",
content=Column(
*[CateringSelectionItem(
article_name=catering_menu_item.name,
article_price=catering_menu_item.price,
article_id=catering_menu_item.item_id,
on_add_callback=self.shopping_cart_and_orders[0].on_add_item,
is_sensitive=(user_id is not None) and not catering_menu_item.is_disabled,
additional_info=catering_menu_item.additional_info,
is_grey=idx % 2 == 0
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC))],
),
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=1,
align_y=0.5
)
),
MainViewContentBox(
Revealer(
header="Cocktails & Longdrinks",
content=Column(
*[CateringSelectionItem(
article_name=catering_menu_item.name,
article_price=catering_menu_item.price,
article_id=catering_menu_item.item_id,
on_add_callback=self.shopping_cart_and_orders[0].on_add_item,
is_sensitive=(user_id is not None) and not catering_menu_item.is_disabled,
additional_info=catering_menu_item.additional_info,
is_grey=idx % 2 == 0
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.BEVERAGE_COCKTAIL))],
),
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=1,
align_y=0.5
)
),
MainViewContentBox(
Revealer(
header="Shots",
content=Column(
*[CateringSelectionItem(
article_name=catering_menu_item.name,
article_price=catering_menu_item.price,
article_id=catering_menu_item.item_id,
on_add_callback=self.shopping_cart_and_orders[0].on_add_item,
is_sensitive=(user_id is not None) and not catering_menu_item.is_disabled,
additional_info=catering_menu_item.additional_info,
is_grey=idx % 2 == 0
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.BEVERAGE_SHOT))],
),
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=1,
align_y=0.5
)
),
MainViewContentBox(
Revealer(
header="Sonstiges",
content=Column(
*[CateringSelectionItem(
article_name=catering_menu_item.name,
article_price=catering_menu_item.price,
article_id=catering_menu_item.item_id,
on_add_callback=self.shopping_cart_and_orders[0].on_add_item,
is_sensitive=(user_id is not None) and not catering_menu_item.is_disabled,
additional_info=catering_menu_item.additional_info,
is_grey=idx % 2 == 0
) for idx, catering_menu_item in enumerate(self.get_menu_items_by_category(self.all_menu_items, CateringMenuItemCategory.NON_FOOD))],
),
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin=1,
align_y=0.5
)
)]
return Column(
# SHOPPING CART
shopping_cart_and_orders_container,
# ITEM SELECTION
*menu,
align_y=0
)
+139
View File
@@ -0,0 +1,139 @@
from datetime import datetime, timedelta
from typing import Optional
from rio import Text, Column, TextStyle, Component, event, TextInput, MultiLineTextInput, Row, Button
from src.ezgg_lan_manager import ConfigurationService, UserService, MailingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
from src.ezgg_lan_manager.types.User import User
class ContactPage(Component):
# Workaround: Can not reassign this value without rio triggering refresh
# Using list to bypass this behavior
last_message_sent: list[datetime] = [datetime(day=1, month=1, year=2000)]
user: Optional[User] = None
e_mail: str = ""
subject: str = ""
message: str = ""
submit_button_is_loading: bool = False
response_message: str = ""
is_success: bool = True
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Kontakt")
if self.session[SessionStorage].user_id is not None:
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
else:
self.user = None
self.e_mail = "" if not self.user else self.user.user_mail
async def on_send_pressed(self) -> None:
error_msg = ""
self.submit_button_is_loading = True
now = datetime.now()
if not self.e_mail:
error_msg = "E-Mail darf nicht leer sein!"
elif not self.subject:
error_msg = "Betreff darf nicht leer sein!"
elif not self.message:
error_msg = "Nachricht darf nicht leer sein!"
elif (now - self.last_message_sent[0]) < timedelta(minutes=1):
error_msg = "Immer mit der Ruhe!"
if error_msg:
self.submit_button_is_loading = False
self.is_success = False
self.response_message = error_msg
return
mail_recipient = self.session[ConfigurationService].get_lan_info().organizer_mail
msg = (f"Kontaktformular vom {now.strftime('%d.%m.%Y %H:%M')}:\n\n"
f"Betreff: {self.subject}\n"
f"Absender: {self.e_mail}\n\n"
f"Inhalt:\n"
f"{self.message}\n")
await self.session[MailingService].send_email("Kontaktformular-Mitteilung", msg, mail_recipient)
self.last_message_sent[0] = datetime.now()
self.submit_button_is_loading = False
self.is_success = True
self.response_message = "Nachricht erfolgreich gesendet!"
def build(self) -> Component:
email_input = TextInput(
label="E-Mail Adresse",
text=self.bind().e_mail,
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True
)
subject_input = TextInput(
label="Betreff",
text=self.bind().subject,
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True
)
message_input = MultiLineTextInput(
label="Deine Nachricht an uns",
text=self.bind().message,
margin_left=1,
margin_right=1,
margin_bottom=1,
min_height=5
)
submit_button = Button(
content=Text(
"Absenden",
style=TextStyle(fill=self.session.theme.success_color, font_size=0.9),
align_x=0.2
),
align_x=0.9,
margin_top=2,
margin_bottom=1,
shape="rectangle",
style="major",
color="primary",
on_press=self.on_send_pressed,
is_loading=self.bind().submit_button_is_loading
)
return Column(
MainViewContentBox(
Column(
Text(
text="Kontakt",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=1,
align_x=0.5
),
email_input,
subject_input,
message_input,
Row(
Text(
text=self.bind().response_message,
style=TextStyle(
fill=self.session.theme.success_color if self.is_success else self.session.theme.danger_color,
font_size=0.9
),
margin_top=2,
margin_bottom=1,
align_x=0.1
),
submit_button,
)
)
),
align_y=0
)
+89
View File
@@ -0,0 +1,89 @@
from __future__ import annotations
from asyncio import sleep
from typing import * # type: ignore
from rio import Component, event, Spacer, Card, Container, Column, Row, TextStyle, Color, Text
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
from src.ezgg_lan_manager import ConfigurationService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
class DbErrorPage(Component):
@event.on_window_size_change
async def on_window_size_change(self) -> None:
self.force_refresh()
@event.on_mount
async def retry_db_connect(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Fehler")
while not await self.session[DatabaseService].is_healthy():
await sleep(2)
self.session.navigate_to("./")
def build(self) -> Component:
content = Card(
content=MainViewContentBox(
content=Text(
text="Ouh-oh, da läuft gerade irgendwas schief.\n\n"
"Unser Team kümmert sich bereits um das Problem.\n\n"
"Du wirst automatisch weitergeleitet sobald das System wieder verfügbar ist.",
margin=2,
style=TextStyle(
fill=self.session.theme.danger_color,
font_size=1.3
),
overflow="wrap"
)
),
color="secondary",
min_width=38,
corner_radius=(0, 0.5, 0, 0)
)
if self.session.window_width > 28:
return Container(
content=Column(
Column(
Row(
Spacer(grow_x=True, grow_y=True),
Card(
content=Spacer(),
color=self.session.theme.neutral_color,
min_width=15,
grow_y=True,
corner_radius=(0.5, 0, 0, 0),
margin_right=0.1
),
content,
Spacer(grow_x=True, grow_y=True),
grow_y=True
),
Row(
Spacer(grow_x=True, grow_y=False),
Card(
content=Text(f"EZGG LAN Manager Version {self.session[ConfigurationService].APP_VERSION} © EZ GG e.V.", align_x=0.5, align_y=0.5, style=TextStyle(fill=self.session.theme.primary_color, font_size=0.5)),
color=self.session.theme.neutral_color,
corner_radius=(0, 0, 0.5, 0.5),
grow_x=False,
grow_y=False,
min_height=1.2,
min_width=53.1,
margin_bottom=3
),
Spacer(grow_x=True, grow_y=False),
grow_y=False
),
margin_top=4
)
),
grow_x=True,
grow_y=True
)
else:
return Text(
"Der EZGG LAN Manager wird\nauf mobilen Endgeräten nur\nim Querformat unterstützt.\nBitte drehe dein Gerät.",
align_x=0.5,
align_y=0.5,
style=TextStyle(fill=Color.from_hex("FFFFFF"), font_size=0.8)
)
+26
View File
@@ -0,0 +1,26 @@
from typing import Optional
from rio import Column, Component, event, Spacer
from src.ezgg_lan_manager import ConfigurationService, UserService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.UserEditForm import UserEditForm
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
from src.ezgg_lan_manager.types.User import User
class EditProfilePage(Component):
user: Optional[User] = None
pfp: Optional[bytes] = None
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Profil bearbeiten")
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
self.pfp = await self.session[UserService].get_profile_picture(self.user.user_id)
def build(self) -> Component:
return Column(
MainViewContentBox(UserEditForm(is_own_profile=True)),
Spacer(grow_y=True)
)
+70
View File
@@ -0,0 +1,70 @@
from rio import Column, Component, event, TextStyle, Text, Revealer
from src.ezgg_lan_manager import ConfigurationService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
FAQ: list[list[str]] = [
["Wie melde ich mich für die LAN an?",
"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 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 Veranstaltungsgebä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 Navigationsleiste."],
["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 Veranstaltungsgebäude sind offizielle Schlafbereiche ausgewiesen. Solange du keine Zugangs-, Durchgangs-, oder Rettungswege blockierst, darfst du überall schlafen."]
]
class FaqPage(Component):
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - FAQ")
def build(self) -> Component:
return Column(
MainViewContentBox(
Column(
Text(
text="FAQ",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=0,
align_x=0.5
),
*[Revealer(
header=question,
content=Text(
text=answer,
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.9
),
margin=1,
overflow="wrap"
),
margin=1,
grow_x=True,
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.8
)
) for question, answer in FAQ]
)
),
align_y=0
)
@@ -0,0 +1,109 @@
from hashlib import sha256
from random import choices
from email_validator import validate_email, EmailNotValidError
from rio import Column, Component, event, Text, TextStyle, TextInput, TextInputChangeEvent, Button
from src.ezgg_lan_manager import ConfigurationService, UserService, MailingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
class ForgotPasswordPage(Component):
def on_email_changed(self, change_event: TextInputChangeEvent) -> None:
try:
validate_email(change_event.text, check_deliverability=False)
self.email_input.is_valid = True
self.submit_button.is_sensitive = True
except EmailNotValidError:
self.email_input.is_valid = False
self.submit_button.is_sensitive = False
async def on_submit_button_pressed(self) -> None:
self.submit_button.is_loading = True
self.submit_button.force_refresh()
lan_info = self.session[ConfigurationService].get_lan_info()
user_service = self.session[UserService]
mailing_service = self.session[MailingService]
user = await user_service.get_user(self.email_input.text.strip())
if user is not None:
new_password = "".join(choices(user_service.ALLOWED_USER_NAME_SYMBOLS, k=16))
user.user_password = sha256(new_password.encode(encoding="utf-8")).hexdigest()
await user_service.update_user(user)
await mailing_service.send_email(
subject=f"Dein neues Passwort für {lan_info.name}",
body=f"Du hast für den EZ-LAN Manager der {lan_info.name} ein neues Passwort angefragt. "
f"Und hier ist es schon:\n\n{new_password}\n\nSolltest du kein neues Passwort angefordert haben, "
f"ignoriere diese E-Mail.\n\nLiebe Grüße\nDein {lan_info.name} - Team",
receiver=self.email_input.text.strip()
)
self.submit_button.is_loading = False
self.email_input.text = ""
self.info_text.text = "Falls für diese E-Mail ein Konto besteht, " \
"bekommst du in den nächsten Minuten ein neues Passwort zugeschickt. " \
"Bitte prüfe dein Spam-Postfach.",
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Passwort vergessen")
def build(self) -> Component:
self.email_input = TextInput(
label="E-Mail Adresse",
text="",
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
on_change=self.on_email_changed
)
self.submit_button = Button(
content=Text(
"Neues Passwort anfordern",
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9),
align_x=0.5
),
grow_x=True,
margin_top=2,
margin_left=1,
margin_right=1,
margin_bottom=1,
shape="rectangle",
style="minor",
color=self.session.theme.secondary_color,
on_press=self.on_submit_button_pressed,
is_sensitive=False
)
self.info_text = Text(
text="",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1
),
margin_top=2,
margin_left=1,
margin_right=1,
margin_bottom=2,
overflow="wrap"
)
return Column(
MainViewContentBox(
content=Column(
Text(
"Passwort vergessen",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
),
self.email_input,
self.submit_button,
self.info_text
)
),
align_y=0,
)
+94
View File
@@ -0,0 +1,94 @@
from typing import Optional
from rio import Column, Component, event, TextStyle, Text, Button, Row, TextInput, Spacer, TextInputChangeEvent
from src.ezgg_lan_manager import ConfigurationService, UserService, TicketingService, SeatingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.types.Seat import Seat
from src.ezgg_lan_manager.types.User import User
class GuestsPage(Component):
table_elements: list[Button] = []
users_with_tickets: list[User] = []
users_with_seats: dict[User, Seat] = {}
user_filter: Optional[str] = None
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Teilnehmer")
user_service = self.session[UserService]
all_users = await user_service.get_all_users()
ticketing_service = self.session[TicketingService]
seating_service = self.session[SeatingService]
u_w_t = []
u_w_s = {}
for user in all_users:
ticket = await ticketing_service.get_user_ticket(user.user_id)
seat = await seating_service.get_user_seat(user.user_id)
if ticket is not None:
u_w_t.append(user)
if seat is not None:
u_w_s[user] = seat
self.users_with_tickets = u_w_t
self.users_with_seats = u_w_s
def on_searchbar_content_change(self, change_event: TextInputChangeEvent) -> None:
self.user_filter = change_event.text
def build(self) -> Component:
if self.user_filter:
users = [user for user in self.users_with_tickets if self.user_filter.lower() in user.user_name or self.user_filter.lower() in str(user.user_id)]
else:
users = self.users_with_tickets
self.table_elements.clear()
for idx, user in enumerate(users):
try:
seat = self.users_with_seats[user]
except KeyError:
seat = None
self.table_elements.append(
Button(
content=Row(Text(text=f"{user.user_id:0>4}", align_x=0, margin_right=1), Text(text=user.user_name, grow_x=True, overflow="ellipsize"),
Text(text="-" if seat is None else seat.seat_id, align_x=1)),
shape="rectangle",
grow_x=True,
color=self.session.theme.hud_color if idx % 2 == 0 else self.session.theme.primary_color
)
)
return Column(
MainViewContentBox(
Column(
Text(
text="Teilnehmer",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
),
TextInput(
label="Suche nach Name oder ID",
margin=1,
margin_left=3,
margin_right=3,
on_change=self.on_searchbar_content_change
),
Button(
content=Row(Text(text="ID ", align_x=0, margin_right=1), Text(text="Benutzername", grow_x=True), Text(text="Sitzplatz", align_x=1)),
shape="rectangle",
grow_x=True,
color=self.session.theme.primary_color,
style="plain-text",
is_sensitive=False
),
*self.table_elements,
Spacer(min_height=1)
)
),
align_y=0
)
+104
View File
@@ -0,0 +1,104 @@
from rio import Text, Column, TextStyle, Component, event, Link, Color
from src.ezgg_lan_manager import ConfigurationService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
class ImprintPage(Component):
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Impressum & DSGVO")
def build(self) -> Component:
return Column(
MainViewContentBox(
Column(
Text(
text="Impressum",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
align_x=0.5
),
Text(
text="Angaben gemäß § 5 TMG:\n\n"
"Einfach Zockem Gaming Gesellschaft e.V.\n"
"Im Elchgrund 18\n"
"35080 Bad Endbach - Bottenhorn\n\n"
"Vertreten durch:\n\n"
"1. Vorsitzender: David Rodenkirchen\n"
"2. Vorsitzender: Julia Albring\n"
"Schatzmeisterin: Jessica Rodenkirchen\n\n"
"Kontakt:\n\n"
"E-Mail: vorstand (at) ezgg-ev.de\n\n"
"Registereintrag:\n\n"
"Eingetragen im Vereinsregister.\n"
"Registergericht: Amtsgericht Marburg\n"
"Registernummer: VR 5837\n\n"
"Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV:\n\n"
"David Rodenkirchen\n"
"Im Elchgrund 18\n"
"35080 Bad Endbach - Bottenhorn\n",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.9
),
margin=2,
overflow="wrap"
)
)
),
MainViewContentBox(
Column(
Text(
text="Datenschutzerklärung",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
align_x=0.5
),
Text(
text="Die Datenschutzerklärung kann über den untenstehenden Link eingesehen werden",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.9
),
margin_top=2,
margin_bottom=0,
overflow="wrap",
align_x=0.5,
grow_x=True,
min_width=30
),
Link(
content=Text(
text="Datenschutzerklärung",
style=TextStyle(
fill=Color.from_hex("000080"),
font_size=0.9,
underlined=True
),
margin_bottom=1,
margin_top=1,
overflow="wrap",
align_x=0.5
),
target_url="https://ezgg-ev.de/privacy",
open_in_new_tab=True
)
)
),
align_y=0
)
@@ -0,0 +1,186 @@
import logging
from dataclasses import field, dataclass
from datetime import datetime
from typing import Optional, Callable
from rio import Column, Component, event, TextStyle, Text, Spacer, PointerEvent, Button, Popup, Card, Row
from src.ezgg_lan_manager import ConfigurationService, CateringService, SeatingService, AccountingService
from src.ezgg_lan_manager.components.CateringManagementOrderDisplay import CateringManagementOrderDisplay
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus
from src.ezgg_lan_manager.types.Seat import Seat
logger = logging.getLogger(__name__.split(".")[-1])
class CateringOrderInfoPopup(Component):
order: Optional[CateringOrder] = None
close_cb: Optional[Callable] = None
def build(self) -> Component:
if not self.order:
return Card(
content=Text(""),
margin=1,
color=self.session.theme.hud_color,
min_width=40,
min_height=40,
on_press=self.close_cb
)
rows = []
is_contrast_line = True
for item, amount in self.order.items.items():
style = TextStyle(
fill=self.session.theme.secondary_color if is_contrast_line else self.session.theme.neutral_color)
is_contrast_line = not is_contrast_line
rows.append(
Row(
Text(f"{amount}x", style=style),
Spacer(),
Text(f"{item.name}", style=style)
)
)
return Card(
content=Column(
Text(f"Bestellung {self.order.order_id}", style=TextStyle(font_size=1.2), margin_bottom=1),
*rows,
Spacer(),
Row(Text("Gesamtpreis:"), Spacer(),
Text(self.session[AccountingService].make_euro_string_from_decimal(self.order.price)))
),
margin=1,
color=self.session.theme.hud_color,
min_width=40,
min_height=40,
on_press=self.close_cb,
corner_radius=0.5,
elevate_on_hover=False,
colorize_on_hover=False
)
@dataclass
class CateringOrderWithSeat:
catering_order: CateringOrder
seat: Optional[Seat]
class ManageCateringPage(Component):
all_orders: list[CateringOrderWithSeat] = field(default_factory=list)
last_updated: Optional[datetime] = None
order_popup_open: bool = False
order_popup_order: Optional[CateringOrder] = None
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Catering Verwaltung")
self.all_orders = await self.populate_seating(await self.session[CateringService].get_orders())
self.last_updated = datetime.now()
@event.periodic(30)
async def update_orders(self) -> None:
polled_orders = await self.session[CateringService].get_orders()
self.all_orders = await self.populate_seating(polled_orders)
self.last_updated = datetime.now()
async def populate_seating(self, orders: list[CateringOrder]) -> list[CateringOrderWithSeat]:
result = []
for order in orders:
seat = await self.session[SeatingService].get_user_seat(order.customer.user_id)
result.append(CateringOrderWithSeat(catering_order=order, seat=seat))
return result
def get_all_pending_orders(self) -> list[CateringOrderWithSeat]:
filtered_list = list(filter(lambda
o: o.catering_order.status != CateringOrderStatus.COMPLETED and o.catering_order.status != CateringOrderStatus.CANCELED,
self.all_orders))
sorted_list = sorted(filtered_list, key=lambda o: o.catering_order.order_date)
return sorted_list
def get_all_completed_orders(self) -> list[CateringOrderWithSeat]:
filtered_list = list(filter(lambda
o: o.catering_order.status == CateringOrderStatus.COMPLETED or o.catering_order.status == CateringOrderStatus.CANCELED,
self.all_orders))
sorted_list = sorted(filtered_list, key=lambda o: o.catering_order.order_date)
return sorted_list
async def order_clicked(self, order: CateringOrder, _: PointerEvent) -> None:
self.order_popup_order = order
self.order_popup_open = True
async def close_cb(self) -> None:
self.order_popup_open = False
def build(self) -> Component:
header_text = Text(
text="Catering Verwaltung",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
)
popup = Popup(
anchor=header_text,
content=CateringOrderInfoPopup(order=self.order_popup_order, close_cb=self.close_cb),
is_open=self.order_popup_open,
position="bottom",
corner_radius=0.5
)
return Column(
MainViewContentBox(
Column(popup)
),
MainViewContentBox(
Column(
Text(
text="Offene Bestellungen",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=0.2,
align_x=0.5
),
Text(
text=f"Letzte Aktualisierung: {'-' if not self.last_updated else self.last_updated.strftime('%H:%M:%S')}",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.7
),
margin_top=0.2,
margin_bottom=0.2,
align_x=0.5
),
Button(
content=Text("Jetzt aktualisieren", style=TextStyle(font_size=0.7), justify="center"),
shape="rectangle",
margin_bottom=1,
on_press=self.update_orders
),
*[CateringManagementOrderDisplay(v.catering_order, v.seat, self.order_clicked) for v in
self.get_all_pending_orders()],
)
),
MainViewContentBox(
Column(
Text(
text="Abgeschlossene Bestellungen",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=0.2,
align_x=0.5
),
*[CateringManagementOrderDisplay(v.catering_order, v.seat, self.order_clicked) for v in
self.get_all_completed_orders()],
)
),
Spacer()
)
@@ -0,0 +1,131 @@
import logging
from asyncio import sleep
from datetime import datetime
from time import strptime
from rio import Column, Component, event, TextStyle, Text
from src.ezgg_lan_manager import ConfigurationService, UserService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.NewsPost import EditableNewsPost
from src.ezgg_lan_manager.services.NewsService import NewsService
from src.ezgg_lan_manager.types.News import News
logger = logging.getLogger(__name__.split(".")[-1])
class ManageNewsPage(Component):
news_posts: list[News] = []
show_success_message = False
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - News Verwaltung")
self.news_posts = (await self.session[NewsService].get_news())[:8]
async def on_new_news_post(self, post: EditableNewsPost) -> None:
# @todo: For some reason, new posts do not appear through a force_refresh, only after visiting the page again
author = await self.session[UserService].get_user(post.author)
if author is None:
logger.warning(f"Tried to set news post author to '{post.author}', which does not exist.")
return
await self.session[NewsService].add_news(News(
news_id=None,
title=post.title,
subtitle=post.subtitle,
content=post.text,
author=author,
news_date=strptime(post.date, "%d.%m.%Y"),
))
self.news_posts = (await self.session[NewsService].get_news())[:8]
self.show_success_message = True
self.force_refresh()
await sleep(3)
self.show_success_message = False
self.force_refresh()
async def on_news_post_changed(self, post: EditableNewsPost) -> None:
author = await self.session[UserService].get_user(post.author)
if author is None:
logger.warning(f"Tried to set news post author to '{post.author}', which does not exist.")
return
await self.session[NewsService].update_news(News(
news_id=post.news_id,
title=post.title,
subtitle=post.subtitle,
content=post.text,
author=author,
news_date=strptime(post.date, "%d.%m.%Y"),
))
self.news_posts = (await self.session[NewsService].get_news())[:8]
async def on_news_post_deleted(self, news_id: int) -> None:
await self.session[NewsService].delete_news(news_id)
self.news_posts = (await self.session[NewsService].get_news())[:8]
def build(self) -> Component:
posts = [EditableNewsPost(
news_id=news.news_id,
title=news.title,
subtitle=news.subtitle,
text=news.content,
date=news.news_date.strftime("%d.%m.%Y"),
author=news.author.user_name,
save_cb=self.on_news_post_changed,
delete_cb=self.on_news_post_deleted
) for news in self.news_posts]
return Column(
MainViewContentBox(
Column(
Text(
text="News Verwaltung",
fill=self.session.theme.background_color,
style=TextStyle(
font_size=1.2
),
margin_top=2,
margin_bottom=0,
align_x=0.5
),
Text(
text="Neuen News Post erstellen",
fill=self.session.theme.background_color,
style=TextStyle(
font_size=1.1
),
margin_top=2,
margin_bottom=0,
align_x=0.5
),
EditableNewsPost(
title="",
subtitle="",
text="",
date=datetime.now().strftime("%d.%m.%Y"),
author="",
save_cb=self.on_new_news_post
),
Text(
text="Post erfolgreich erstellt",
fill=self.session.theme.success_color,
style=TextStyle(
font_size=0.7 if self.show_success_message else 0
),
margin_top=0.1,
margin_bottom=0,
align_x=0.5
),
Text(
text="Bisherige Posts",
fill=self.session.theme.background_color,
style=TextStyle(
font_size=1.1
),
margin_top=2,
margin_bottom=0,
align_x=0.5
),
*posts
)
),
align_y=0
)
@@ -0,0 +1,32 @@
import logging
from rio import Column, Component, event, TextStyle, Text, Spacer
from src.ezgg_lan_manager import ConfigurationService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
logger = logging.getLogger(__name__.split(".")[-1])
class ManageTournamentsPage(Component):
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Turnier Verwaltung")
def build(self) -> Component:
return Column(
MainViewContentBox(
Column(
Text(
text="Turnier Verwaltung",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
)
)
),
Spacer()
)
@@ -0,0 +1,285 @@
import logging
from dataclasses import field
from typing import Optional
from rio import Column, Component, event, TextStyle, Text, TextInput, ThemeContextSwitcher, Grid, \
PointerEventListener, PointerEvent, Rectangle, CursorStyle, Color, TextInputChangeEvent, Spacer, Row, Switch, \
SwitchChangeEvent, EventHandler
from src.ezgg_lan_manager import ConfigurationService, UserService, AccountingService, SeatingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.NewTransactionForm import NewTransactionForm
from src.ezgg_lan_manager.components.UserEditForm import UserEditForm
from src.ezgg_lan_manager.services.AccountingService import InsufficientFundsError
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
from src.ezgg_lan_manager.types.Transaction import Transaction
from src.ezgg_lan_manager.types.User import User
logger = logging.getLogger(__name__.split(".")[-1])
class ClickableGridContent(Component):
text: str = ""
is_hovered: bool = False
clicked_cb: EventHandler[str] = None
async def on_mouse_enter(self, _: PointerEvent) -> None:
self.is_hovered = True
async def on_mouse_leave(self, _: PointerEvent) -> None:
self.is_hovered = False
async def on_mouse_click(self, _: PointerEvent) -> None:
await self.call_event_handler(self.clicked_cb, self.text)
def build(self) -> Component:
return PointerEventListener(
content=Rectangle(
content=Text(
self.text,
style=TextStyle(fill=self.session.theme.success_color) if self.is_hovered else TextStyle(
fill=self.session.theme.background_color),
grow_x=True
),
fill=Color.TRANSPARENT,
cursor=CursorStyle.POINTER
),
on_pointer_enter=self.on_mouse_enter,
on_pointer_leave=self.on_mouse_leave,
on_press=self.on_mouse_click
)
class ManageUsersPage(Component):
selected_user: Optional[User] = None
all_users: Optional[list] = None
search_results: list[User] = field(default_factory=list)
accounting_section_result_text: str = ""
accounting_section_result_success: bool = True
user_account_balance: str = "0.00 €"
user_seat: str = "-"
is_user_account_locked: bool = False
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Nutzer Verwaltung")
self.all_users = await self.session[UserService].get_all_users()
self.search_results = self.all_users
async def on_user_clicked(self, user_name: str) -> None:
self.selected_user = next(filter(lambda user: user.user_name == user_name, self.all_users))
user_account_balance_raw = await self.session[AccountingService].get_balance(self.selected_user.user_id)
self.user_account_balance = AccountingService.make_euro_string_from_decimal(user_account_balance_raw)
seat = await self.session[SeatingService].get_user_seat(self.selected_user.user_id)
self.user_seat = seat.seat_id if seat else "-"
self.is_user_account_locked = not self.selected_user.is_active
async def on_search_parameters_changed(self, e: TextInputChangeEvent) -> None:
self.search_results = list(
filter(lambda user: (e.text.lower() in user.user_name.lower()) or e.text.lower() in str(user.user_id),
self.all_users))
async def change_account_active(self, _: SwitchChangeEvent) -> None:
self.selected_user.is_active = not self.is_user_account_locked
await self.session[UserService].update_user(self.selected_user)
async def on_new_transaction(self, transaction: Transaction) -> None:
if not self.session[SessionStorage].is_team_member: # Better safe than sorry
return
logger.info(f"Got new transaction for user with ID '{transaction.user_id}' over "
f"{'-' if transaction.is_debit else '+'}"
f"{AccountingService.make_euro_string_from_decimal(transaction.value)} "
f"with reference '{transaction.reference}'")
if transaction.is_debit:
try:
await self.session[AccountingService].remove_balance(
transaction.user_id,
transaction.value,
transaction.reference
)
except InsufficientFundsError:
self.accounting_section_result_text = "Guthaben nicht ausreichend!"
self.accounting_section_result_success = False
return
else:
await self.session[AccountingService].add_balance(
transaction.user_id,
transaction.value,
transaction.reference
)
self.accounting_section_result_text = f"Guthaben {'entfernt' if transaction.is_debit else 'hinzugefügt'}!"
self.accounting_section_result_success = True
def build(self) -> Component:
return Column(
MainViewContentBox(
Column(
Text(
text="Nutzersuche",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
),
TextInput(
label="Nutzername oder ID",
margin=1,
on_change=self.on_search_parameters_changed
),
ThemeContextSwitcher(
Grid(
[
Text("Nutzername", margin_bottom=1, grow_x=True, style=TextStyle(font_size=1.1)),
Text("Nutzer-ID", margin_bottom=1, style=TextStyle(font_size=1.1))
],
*[[
ClickableGridContent(text=user.user_name, clicked_cb=self.on_user_clicked),
Text(
str(user.user_id),
justify="right"
)
] for user in self.search_results],
row_spacing=0.2,
margin=1
),
color="primary"
)
)
),
MainViewContentBox(
Column(
Text(
text="Konto & Sitzplatz",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
),
Row(
Text(
text="Kontostand:",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1
),
margin_top=0.5,
margin_bottom=1,
margin_left=2
),
Text(
text=self.bind().user_account_balance,
style=TextStyle(
fill=self.session.theme.neutral_color,
font_size=1
),
margin_top=0.5,
margin_bottom=1,
margin_right=2,
justify="right"
),
),
Row(
Text(
text="Kontosperrung:",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1
),
margin_top=0.5,
margin_bottom=1,
margin_left=2,
grow_x=True
),
ThemeContextSwitcher(
content=Switch(
is_on=self.bind().is_user_account_locked,
margin_top=0.5,
margin_bottom=1,
margin_right=2,
on_change=self.change_account_active
),
color="primary"
),
),
Row(
Text(
text="Sitzplatz:",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1
),
margin_top=0.5,
margin_bottom=1,
margin_left=2
),
Text(
text=self.bind().user_seat,
style=TextStyle(
fill=self.session.theme.neutral_color,
font_size=1
),
margin_top=0.5,
margin_bottom=1,
margin_right=2,
justify="right"
),
),
Text(
text="Geld hinzufügen/entfernen",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1
),
margin_top=0.5,
margin_bottom=1,
align_x=0.5
),
NewTransactionForm(user=self.selected_user, new_transaction_cb=self.on_new_transaction),
Text(
text=self.bind().accounting_section_result_text,
style=TextStyle(
fill=self.session.theme.success_color if self.accounting_section_result_success else self.session.theme.danger_color
),
margin_left=1,
margin_bottom=1
)
)
) if self.selected_user else Spacer(),
MainViewContentBox(
Column(
Text(
text="Allgemeines",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
) if self.selected_user else Spacer(),
UserEditForm(
is_own_profile=False,
user=self.selected_user
) if self.selected_user else Text(
text="Bitte Nutzer auswählen...",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
)
)
),
align_y=0
)
+27
View File
@@ -0,0 +1,27 @@
from rio import Column, Component, event
from src.ezgg_lan_manager import ConfigurationService, NewsService
from src.ezgg_lan_manager.components.NewsPost import NewsPost
from src.ezgg_lan_manager.types.News import News
class NewsPage(Component):
news_posts: list[News] = []
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Neuigkeiten")
self.news_posts = (await self.session[NewsService].get_news())[:8]
def build(self) -> Component:
posts = [NewsPost(
title=news.title,
subtitle=news.subtitle,
text=news.content,
date=news.news_date.strftime("%d.%m.%Y"),
author=news.author.user_name
) for news in self.news_posts]
return Column(
*posts,
align_y=0,
)
+141
View File
@@ -0,0 +1,141 @@
from rio import Column, Component, event, Text, Spacer, Row, Link
from src.ezgg_lan_manager import ConfigurationService, TicketingService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
class OverviewPage(Component):
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Übersicht")
def build(self) -> Component:
lan_info = self.session[ConfigurationService].get_lan_info()
return Column(
MainViewContentBox(
Column(
Text(lan_info.name, font_size=2, justify="center", fill=self.session.theme.neutral_color, margin_top=0.5),
Text(f"Edition {lan_info.iteration}", font_size=0.9, justify="center", fill=self.session.theme.neutral_color, margin_bottom=1.5)
)
),
MainViewContentBox(
Column(
Text("Allgemeines", font_size=2, justify="center", fill=self.session.theme.neutral_color, margin_top=0.5, margin_bottom=1),
Column(
Row(
Text("Wann?", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Text(f"{lan_info.date_from.strftime("%d.%m.")} bis {lan_info.date_till.strftime("%d.%m.%Y")}", fill=self.session.theme.neutral_color, margin_right=1),
margin_bottom=0.3
),
Row(
Text("Wo?", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Link(Text(f"DGH Donsbach", fill=self.session.theme.secondary_color, margin_right=1), target_url="https://maps.app.goo.gl/3Zyue776A22jdoxz5", open_in_new_tab=True),
margin_bottom=0.3
),
Row(
Text("Einlass", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Text(lan_info.date_from.strftime("Freitag %H:%M Uhr"), fill=self.session.theme.neutral_color, margin_right=1),
margin_bottom=0.3
),
Row(
Text("Ende", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Text(lan_info.date_till.strftime("Sonntag %H:%M Uhr"), fill=self.session.theme.neutral_color, margin_right=1),
margin_bottom=0.3
),
Row(
Text("Anmeldung", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Text("Geöffnet", fill=self.session.theme.success_color, margin_right=1),
margin_bottom=0.3
),
Row(
Text("Teilnehmer", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Text(str(self.session[TicketingService].get_total_tickets()), fill=self.session.theme.neutral_color, margin_right=1),
margin_bottom=0.3
)
,
Row(
Text("Ticket Preise", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Link(Text(f"Preisliste", fill=self.session.theme.secondary_color, margin_right=1), target_url="./buy_ticket", open_in_new_tab=False),
margin_bottom=0.3
)
)
)
),
MainViewContentBox(
Column(
Text("Technisches", font_size=2, justify="center", fill=self.session.theme.neutral_color, margin_top=0.5, margin_bottom=1),
Column(
Row(
Text("Internet", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Text(f"60/20 Mbit/s (down/up)", fill=self.session.theme.neutral_color, margin_right=1),
margin_bottom=0.3
),
Row(
Text("Routing", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Text(f"Flaches Netz", fill=self.session.theme.neutral_color, margin_right=1),
margin_bottom=0.3
),
Row(
Text("WLAN", fill=self.session.theme.neutral_color, margin_left=1),
Spacer(),
Text(f"vorhanden", fill=self.session.theme.neutral_color, margin_right=1),
margin_bottom=0.3
)
)
)
),
MainViewContentBox(
Column(
Text("Sonstiges", font_size=2, justify="center", fill=self.session.theme.neutral_color, margin_top=0.5, margin_bottom=1),
Column(
Row(
Text("Schlafen", fill=self.session.theme.neutral_color, margin_left=1, justify="center"),
margin_bottom=0.3
),
Row(
Text("Es steht ein Schlafsaal zur Verfügung. Nach der Eröffnung steht auch die Bühne als Schlafbereich zur Verfügung.", font_size=0.7,
fill=self.session.theme.neutral_color, margin_left=1, overflow="wrap"),
margin_bottom=0.3
),
Row(
Text("Essen & Trinken", fill=self.session.theme.neutral_color, margin_left=1, justify="center"),
margin_bottom=0.3
),
Row(
Text("Wir sorgen für euer leibliches Wohl, ihr dürft aber auch eure eigenen Speißen und Getränke mitbringen.", font_size=0.7, fill=self.session.theme.neutral_color, margin_left=1, overflow="wrap"),
margin_bottom=0.3
),
Row(
Text("Parken", fill=self.session.theme.neutral_color, margin_left=1, justify="center"),
margin_bottom=0.3
),
Row(
Text("Vor der Halle sind ausreichend Parkplätze vorhanden.", font_size=0.7, fill=self.session.theme.neutral_color, margin_left=1, overflow="wrap"),
margin_bottom=0.3
)
)
)
),
MainViewContentBox(
Column(
Text("Turniere & Ablauf", font_size=2, justify="center", fill=self.session.theme.neutral_color, margin_top=0.5, margin_bottom=1),
Column(
Row(
Text("Zum aktuellen Zeitpunkt steht noch nicht fest welche Turniere gespielt werden. Wir planen diverse Online- und Offline Turniere mit Preisen durchzuführen. Weitere Informationen gibt es, sobald sie kommen, auf der NEWS- und Turnier-Seite.", font_size=0.7,
fill=self.session.theme.neutral_color, margin_left=1, overflow="wrap"),
margin_bottom=0.3
)
)
)
),
Spacer()
)
@@ -0,0 +1,22 @@
from rio import Column, Component, event
from src.ezgg_lan_manager import ConfigurationService
from src.ezgg_lan_manager.components.NewsPost import NewsPost
class PlaceholderPage(Component):
placeholder_name: str
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - {self.placeholder_name}")
def build(self) -> Component:
return Column(
NewsPost(
title="Platzhalter",
text=f"Dies ist die Platzhalterseite für {self.placeholder_name}.",
date="99.99.9999"
),
align_y=0,
)
+178
View File
@@ -0,0 +1,178 @@
import logging
from email_validator import validate_email, EmailNotValidError
from rio import Column, Component, event, Text, TextStyle, TextInput, TextInputChangeEvent, Button
from src.ezgg_lan_manager import ConfigurationService, UserService, MailingService
from src.ezgg_lan_manager.components.AnimatedText import AnimatedText
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
MINIMUM_PASSWORD_LENGTH = 6
logger = logging.getLogger(__name__.split(".")[-1])
class RegisterPage(Component):
def on_pw_focus_loss(self, _: TextInputChangeEvent) -> None:
if not (self.pw_1.text == self.pw_2.text) or len(self.pw_1.text) < MINIMUM_PASSWORD_LENGTH:
self.pw_1.is_valid = False
self.pw_2.is_valid = False
return
self.pw_1.is_valid = True
self.pw_2.is_valid = True
def on_email_focus_loss(self, change_event: TextInputChangeEvent) -> None:
try:
validate_email(change_event.text, check_deliverability=False)
self.email_input.is_valid = True
except EmailNotValidError:
self.email_input.is_valid = False
def on_user_name_focus_loss(self, _: TextInputChangeEvent) -> None:
current_text = self.user_name_input.text
if len(current_text) > UserService.MAX_USERNAME_LENGTH:
self.user_name_input.text = current_text[:UserService.MAX_USERNAME_LENGTH]
async def on_submit_button_pressed(self) -> None:
self.submit_button.is_loading = True
self.submit_button.force_refresh()
if len(self.user_name_input.text) < 1:
await self.animated_text.display_text(False, "Nutzername darf nicht leer sein!")
self.submit_button.is_loading = False
return
if not (self.pw_1.text == self.pw_2.text):
await self.animated_text.display_text(False, "Passwörter stimmen nicht überein!")
self.submit_button.is_loading = False
return
if len(self.pw_1.text) < MINIMUM_PASSWORD_LENGTH:
await self.animated_text.display_text(False, f"Passwort muss mindestens {MINIMUM_PASSWORD_LENGTH} Zeichen lang sein!")
self.submit_button.is_loading = False
return
if not self.email_input.is_valid or len(self.email_input.text) < 3:
await self.animated_text.display_text(False, "E-Mail Adresse ungültig!")
self.submit_button.is_loading = False
return
user_service = self.session[UserService]
mailing_service = self.session[MailingService]
lan_info = self.session[ConfigurationService].get_lan_info()
if await user_service.get_user(self.email_input.text) is not None or await user_service.get_user(self.user_name_input.text) is not None:
await self.animated_text.display_text(False, "Benutzername oder E-Mail bereits regestriert!")
self.submit_button.is_loading = False
return
try:
new_user = await user_service.create_user(self.user_name_input.text, self.email_input.text, self.pw_1.text)
if not new_user:
logger.warning(f"UserService.create_user returned: {new_user}") # ToDo: Seems like the user is created fine, even if not returned #FixMe
except Exception as e:
logger.error(f"Unknown error during new user registration: {e}")
await self.animated_text.display_text(False, "Es ist ein unbekannter Fehler aufgetreten :(")
self.submit_button.is_loading = False
return
await mailing_service.send_email(
subject="Erfolgreiche Registrierung",
body=f"Hallo {self.user_name_input.text},\n\n"
f"Du hast dich erfolgreich beim EZ-LAN Manager für {lan_info.name} {lan_info.iteration} registriert.\n\n"
f"Wenn du dich nicht registriert hast, kontaktiere bitte unser Team über unsere Homepage.\n\n"
f"Liebe Grüße\nDein {lan_info.name} - Team",
receiver=self.email_input.text
)
self.submit_button.is_loading = False
await self.animated_text.display_text(True, "Erfolgreich registriert!")
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Registrieren")
def build(self) -> Component:
self.user_name_input = TextInput(
label="Benutzername",
text="",
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
on_lose_focus=self.on_user_name_focus_loss
)
self.email_input = TextInput(
label="E-Mail Adresse",
text="",
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
on_lose_focus=self.on_email_focus_loss
)
self.pw_1 = TextInput(
label="Passwort",
text="",
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
is_secret=True,
on_lose_focus=self.on_pw_focus_loss
)
self.pw_2 = TextInput(
label="Passwort wiederholen",
text="",
margin_left=1,
margin_right=1,
margin_bottom=1,
grow_x=True,
is_secret=True,
on_lose_focus=self.on_pw_focus_loss
)
self.submit_button = Button(
content=Text(
"Registrieren",
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9),
align_x=0.5
),
grow_x=True,
margin_top=2,
margin_left=1,
margin_right=1,
margin_bottom=1,
shape="rectangle",
style="minor",
color=self.session.theme.secondary_color,
on_press=self.on_submit_button_pressed
)
self.animated_text = AnimatedText(
margin_top=2,
margin_left=1,
margin_right=1,
margin_bottom=2
)
return Column(
MainViewContentBox(
content=Column(
Text(
"Neues Konto anlegen",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=2,
align_x=0.5
),
self.user_name_input,
self.email_input,
self.pw_1,
self.pw_2,
self.submit_button,
self.animated_text
)
),
align_y=0,
)
+192
View File
@@ -0,0 +1,192 @@
from rio import Column, Component, event, TextStyle, Text, Revealer
from src.ezgg_lan_manager import ConfigurationService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
RULES: list[str] = [
"Respektvolles Verhalten: Sei höflich und respektvoll gegenüber anderen Gästen und dem Team.",
"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."
]
AGB: dict[str, list[str]] = {
"§1": [
"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": [
"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": [
"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": [
"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."
]
}
class RulesPage(Component):
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Regeln & AGB")
def build(self) -> Component:
return Column(
MainViewContentBox(
Column(
Text(
text="Regeln",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=0,
align_x=0.5
),
Text(
text="(AGB's in verständlichem deutsch)",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.5
),
margin_top=0.5,
margin_bottom=2,
align_x=0.5
),
*[Text(
f"{idx + 1}. {rule}",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.9
),
margin_bottom=0.8,
margin_left=1,
margin_right=1,
overflow="wrap"
) for idx, rule in enumerate(RULES)],
)
),
MainViewContentBox(
Column(
Text(
text="AGB",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=1,
align_x=0.5
),
Revealer(
header="§ 1 Allgemeine Bestimmungen",
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1
),
margin=1,
margin_top=2,
content=Column(
*[Text(
f"{idx + 1}. {rule}",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.8
),
margin_bottom=0.8,
margin_left=1,
margin_right=1,
overflow="wrap"
) for idx, rule in enumerate(AGB["§1"])]
)
),
Revealer(
header="§ 2 Teilnahmevoraussetzungen",
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1
),
margin=1,
margin_top=0,
content=Column(
*[Text(
f"{idx + 1}. {rule}",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.8
),
margin_bottom=0.8,
margin_left=1,
margin_right=1,
overflow="wrap"
) for idx, rule in enumerate(AGB["§2"])]
)
),
Revealer(
header="§ 3 Verhaltensregeln",
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1
),
margin=1,
margin_top=0,
content=Column(
*[Text(
f"{idx + 1}. {rule}",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.8
),
margin_bottom=0.8,
margin_left=1,
margin_right=1,
overflow="wrap"
) for idx, rule in enumerate(AGB["§3"])]
)
),
Revealer(
header="§ 4 Internetzugang",
header_style=TextStyle(
fill=self.session.theme.background_color,
font_size=1
),
margin=1,
margin_top=0,
content=Column(
*[Text(
f"{idx + 1}. {rule}",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.8
),
margin_bottom=0.8,
margin_left=1,
margin_right=1,
overflow="wrap"
) for idx, rule in enumerate(AGB["§4"])]
)
)
)
),
align_y=0
)
@@ -0,0 +1,151 @@
import logging
from asyncio import sleep
from decimal import Decimal
from typing import Optional
from rio import Text, Column, TextStyle, Component, event, PressEvent, ProgressCircle
from src.ezgg_lan_manager import ConfigurationService, SeatingService, TicketingService, UserService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
from src.ezgg_lan_manager.components.SeatingPlan import SeatingPlan, SeatingPlanLegend
from src.ezgg_lan_manager.components.SeatingPlanInfoBox import SeatingPlanInfoBox
from src.ezgg_lan_manager.components.SeatingPurchaseBox import SeatingPurchaseBox
from src.ezgg_lan_manager.services.SeatingService import NoTicketError, SeatNotFoundError, WrongCategoryError, SeatAlreadyTakenError
from src.ezgg_lan_manager.types.Seat import Seat
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
from src.ezgg_lan_manager.types.User import User
logger = logging.getLogger(__name__.split(".")[-1])
class SeatingPlanPage(Component):
seating_info: Optional[list[Seat]] = None
current_seat_id: Optional[str] = None
current_seat_occupant: Optional[str] = None
current_seat_price: Decimal = Decimal("0")
current_seat_is_blocked: bool = False
user: Optional[User] = None
show_info_box: bool = True
show_purchase_box: bool = False
purchase_box_loading: bool = False
purchase_box_success_msg: Optional[str] = None
purchase_box_error_msg: Optional[str] = None
seating_info_text = ""
is_booking_blocked: bool = False
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Sitzplan")
self.seating_info = await self.session[SeatingService].get_seating()
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
if not self.user:
self.is_booking_blocked = True
else:
for seat in self.seating_info:
if not seat.user or not self.user:
continue
if seat.user.user_id == self.user.user_id:
self.is_booking_blocked = True
async def on_seat_clicked(self, seat_id: str, _: PressEvent) -> None:
self.seating_info_text = ""
self.show_info_box = True
self.show_purchase_box = False
seat = next(filter(lambda s: s.seat_id == seat_id, self.seating_info), None)
if not seat:
return
self.current_seat_is_blocked = seat.is_blocked
self.current_seat_id = seat.seat_id
ticket_info = self.session[TicketingService].get_ticket_info_by_category(seat.category)
price = Decimal("0") if not ticket_info else ticket_info.price
self.current_seat_price = price
if seat.user:
self.current_seat_occupant = seat.user.user_name
else:
self.current_seat_occupant = None
async def on_info_clicked(self, text: str) -> None:
self.show_info_box = True
self.show_purchase_box = False
self.current_seat_id = None
self.seating_info_text = text
def set_error(self, msg: str) -> None:
self.purchase_box_error_msg = msg
self.purchase_box_success_msg = None
def set_success(self, msg: str) -> None:
self.purchase_box_error_msg = None
self.purchase_box_success_msg = msg
async def on_purchase_clicked(self) -> None:
self.show_info_box = False
self.show_purchase_box = True
async def on_purchase_confirmed(self) -> None:
self.purchase_box_loading = True
self.force_refresh()
await sleep(0.5)
try:
await self.session[SeatingService].seat_user(self.user.user_id, self.current_seat_id)
except (NoTicketError, WrongCategoryError):
self.set_error("Du besitzt kein gültiges Ticket für diesen Platz")
except SeatNotFoundError:
self.set_error("Der angegebene Sitzplatz existiert nicht")
except SeatAlreadyTakenError:
self.set_error("Dieser Platz ist bereits vergeben")
except Exception as e:
self.set_error("Ein unbekannter Fehler ist aufgetreten")
logger.error(e)
else:
self.set_success("Platz erfolgreich gebucht!")
self.purchase_box_loading = False
await self.on_populate()
async def on_purchase_cancelled(self) -> None:
self.purchase_box_loading = False
self.show_info_box = True
self.show_purchase_box = False
self.purchase_box_error_msg = None
self.purchase_box_success_msg = None
def build(self) -> Component:
if not self.seating_info:
return Column(
MainViewContentBox(
ProgressCircle(
color="secondary",
align_x=0.5,
margin_top=2,
margin_bottom=2
)
),
align_y=0
)
return Column(
MainViewContentBox(
Column(
SeatingPlanInfoBox(seat_id=self.current_seat_id, seat_occupant=self.current_seat_occupant, seat_price=self.current_seat_price,
is_blocked=self.current_seat_is_blocked, is_booking_blocked=self.is_booking_blocked, show=self.show_info_box,
purchase_cb=self.on_purchase_clicked, override_text=self.seating_info_text),
SeatingPurchaseBox(
show=self.show_purchase_box,
seat_id=self.current_seat_id,
is_loading=self.purchase_box_loading,
confirm_cb=self.on_purchase_confirmed,
cancel_cb=self.on_purchase_cancelled,
error_msg=self.purchase_box_error_msg,
success_msg=self.purchase_box_success_msg
)
)
),
MainViewContentBox(
SeatingPlan(seat_clicked_cb=self.on_seat_clicked, seating_info=self.seating_info, info_clicked_cb=self.on_info_clicked) if self.seating_info else
Column(ProgressCircle(color=self.session.theme.secondary_color, margin=3),
Text("Sitzplan wird geladen", style=TextStyle(fill=self.session.theme.neutral_color), align_x=0.5, margin=1))
),
MainViewContentBox(
SeatingPlanLegend(),
),
align_y=0
)
+38
View File
@@ -0,0 +1,38 @@
from rio import Column, Component, event, TextStyle, Text
from src.ezgg_lan_manager import ConfigurationService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
class PAGENAME(Component):
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - PAGENAME")
def build(self) -> Component:
return Column(
MainViewContentBox(
Column(
Text(
text="HEADER",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=0,
align_x=0.5
),
Text(
text="BASIC TEXT",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.9
),
margin=1,
overflow="wrap"
)
)
),
align_y=0
)
@@ -0,0 +1,38 @@
from rio import Column, Component, event, TextStyle, Text
from src.ezgg_lan_manager import ConfigurationService
from src.ezgg_lan_manager.components.MainViewContentBox import MainViewContentBox
class TournamentsPage(Component):
@event.on_populate
async def on_populate(self) -> None:
await self.session.set_title(f"{self.session[ConfigurationService].get_lan_info().name} - Turniere")
def build(self) -> Component:
return Column(
MainViewContentBox(
Column(
Text(
text="Turniere",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=1.2
),
margin_top=2,
margin_bottom=0,
align_x=0.5
),
Text(
text="Aktuell ist noch kein Turnierplan hinterlegt.",
style=TextStyle(
fill=self.session.theme.background_color,
font_size=0.9
),
margin=1,
overflow="wrap"
)
)
),
align_y=0
)
+22
View File
@@ -0,0 +1,22 @@
from .BasePage import BasePage
from .NewsPage import NewsPage
from .PlaceholderPage import PlaceholderPage
from .Account import AccountPage
from .EditProfile import EditProfilePage
from .ForgotPassword import ForgotPasswordPage
from .RegisterPage import RegisterPage
from .ImprintPage import ImprintPage
from .ContactPage import ContactPage
from .RulesPage import RulesPage
from .FaqPage import FaqPage
from .TournamentsPage import TournamentsPage
from .GuestsPage import GuestsPage
from .CateringPage import CateringPage
from .DbErrorPage import DbErrorPage
from .SeatingPlanPage import SeatingPlanPage
from .BuyTicketPage import BuyTicketPage
from .ManageNewsPage import ManageNewsPage
from .ManageUsersPage import ManageUsersPage
from .ManageCateringPage import ManageCateringPage
from .ManageTournamentsPage import ManageTournamentsPage
from .OverviewPage import OverviewPage