rename lan
This commit was merged in pull request #22.
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import logging
|
||||
|
||||
from from_root import from_root
|
||||
|
||||
from src.ezgg_lan_manager.services import *
|
||||
from src.ezgg_lan_manager.services.AccountingService import AccountingService
|
||||
from src.ezgg_lan_manager.services.CateringService import CateringService
|
||||
from src.ezgg_lan_manager.services.ConfigurationService import ConfigurationService
|
||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
||||
from src.ezgg_lan_manager.services.LocalDataService import LocalDataService
|
||||
from src.ezgg_lan_manager.services.MailingService import MailingService
|
||||
from src.ezgg_lan_manager.services.NewsService import NewsService
|
||||
from src.ezgg_lan_manager.services.ReceiptPrintingService import ReceiptPrintingService
|
||||
from src.ezgg_lan_manager.services.SeatingService import SeatingService
|
||||
from src.ezgg_lan_manager.services.TicketingService import TicketingService
|
||||
from src.ezgg_lan_manager.services.UserService import UserService
|
||||
from src.ezgg_lan_manager.types import *
|
||||
|
||||
# Inits services in the correct order
|
||||
def init_services() -> tuple[AccountingService, CateringService, ConfigurationService, DatabaseService, MailingService, NewsService, SeatingService, TicketingService, UserService, LocalDataService, ReceiptPrintingService]:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
configuration_service = ConfigurationService(from_root("config.toml"))
|
||||
db_service = DatabaseService(configuration_service.get_database_configuration())
|
||||
user_service = UserService(db_service)
|
||||
accounting_service = AccountingService(db_service)
|
||||
news_service = NewsService(db_service)
|
||||
mailing_service = MailingService(configuration_service)
|
||||
ticketing_service = TicketingService(configuration_service.get_ticket_info(), db_service, accounting_service)
|
||||
seating_service = SeatingService(configuration_service.get_lan_info(), db_service, ticketing_service)
|
||||
receipt_printing_service = ReceiptPrintingService(seating_service, configuration_service.get_receipt_printing_configuration(), configuration_service.DEV_MODE_ACTIVE)
|
||||
catering_service = CateringService(db_service, accounting_service, user_service, receipt_printing_service)
|
||||
local_data_service = LocalDataService()
|
||||
|
||||
|
||||
return accounting_service, catering_service, configuration_service, db_service, mailing_service, news_service, seating_service, ticketing_service, user_service, local_data_service, receipt_printing_service
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,38 @@
|
||||
from asyncio import sleep
|
||||
|
||||
from rio import Text, Component, TextStyle
|
||||
|
||||
|
||||
class AnimatedText(Component):
|
||||
def __post_init__(self) -> None:
|
||||
self._display_printing: list[bool] = [False]
|
||||
self.text_comp = Text("")
|
||||
|
||||
async def display_text(self, success: bool, text: str, speed: float = 0.06, font_size: float = 0.9) -> None:
|
||||
if self._display_printing[0]:
|
||||
return
|
||||
else:
|
||||
self._display_printing[0] = True
|
||||
self.text_comp.text = ""
|
||||
if success:
|
||||
self.text_comp.style = TextStyle(
|
||||
fill=self.session.theme.success_color,
|
||||
font_size=font_size
|
||||
)
|
||||
for c in text:
|
||||
self.text_comp.text = self.text_comp.text + c
|
||||
self.text_comp.force_refresh()
|
||||
await sleep(speed)
|
||||
else:
|
||||
self.text_comp.style = TextStyle(
|
||||
fill=self.session.theme.danger_color,
|
||||
font_size=font_size
|
||||
)
|
||||
for c in text:
|
||||
self.text_comp.text = self.text_comp.text + c
|
||||
self.text_comp.force_refresh()
|
||||
await sleep(speed)
|
||||
self._display_printing[0] = False
|
||||
|
||||
def build(self) -> Component:
|
||||
return self.text_comp
|
||||
@@ -0,0 +1,31 @@
|
||||
from typing import Callable
|
||||
from decimal import Decimal
|
||||
|
||||
import rio
|
||||
from rio import Component, Row, Text, IconButton, TextStyle
|
||||
|
||||
from src.ezgg_lan_manager import AccountingService
|
||||
|
||||
MAX_LEN = 24
|
||||
|
||||
class CateringCartItem(Component):
|
||||
article_name: str
|
||||
article_price: Decimal
|
||||
article_id: int
|
||||
list_id: int
|
||||
remove_item_cb: Callable
|
||||
|
||||
@staticmethod
|
||||
def ellipsize_string(string: str) -> str:
|
||||
if len(string) <= MAX_LEN:
|
||||
return string
|
||||
|
||||
return string[:MAX_LEN - 3] + "..."
|
||||
|
||||
def build(self) -> rio.Component:
|
||||
return Row(
|
||||
Text(self.ellipsize_string(self.article_name), align_x=0, overflow="wrap", min_width=19, style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
|
||||
Text(AccountingService.make_euro_string_from_decimal(self.article_price), style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
|
||||
IconButton(icon="material/close", min_size=2, color=self.session.theme.danger_color, style="plain-text", on_press=lambda: self.remove_item_cb(self.list_id)),
|
||||
proportions=(19, 5, 2)
|
||||
)
|
||||
@@ -0,0 +1,103 @@
|
||||
from functools import partial
|
||||
from typing import Optional, Callable
|
||||
|
||||
from rio import Component, Row, Card, Column, Text, TextStyle, Spacer, PointerEventListener, Button
|
||||
|
||||
from src.ezgg_lan_manager.services.CateringService import CateringService
|
||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus
|
||||
from src.ezgg_lan_manager.types.Seat import Seat
|
||||
|
||||
class CateringManagementOrderDisplayStatusButton(Component):
|
||||
status: CateringOrderStatus
|
||||
clicked_cb: Callable
|
||||
def build(self) -> Component:
|
||||
return Button(
|
||||
content=Text(
|
||||
CateringOrder.translate_order_status(self.status)
|
||||
),
|
||||
shape="rectangle",
|
||||
on_press=partial(self.clicked_cb, self.status)
|
||||
)
|
||||
|
||||
|
||||
class CateringManagementOrderDisplay(Component):
|
||||
order: CateringOrder
|
||||
seat: Optional[Seat]
|
||||
clicked_cb: Callable
|
||||
|
||||
def format_order_status(self, status: CateringOrderStatus) -> Text:
|
||||
status_text = CateringOrder.translate_order_status(status)
|
||||
|
||||
color = self.session.theme.warning_color
|
||||
if status == CateringOrderStatus.DELAYED or status == CateringOrderStatus.CANCELED:
|
||||
color = self.session.theme.danger_color
|
||||
elif status == CateringOrderStatus.COMPLETED:
|
||||
color = self.session.theme.success_color
|
||||
|
||||
return Text(text=status_text, style=TextStyle(fill=color))
|
||||
|
||||
async def status_button_clicked(self, new_status: CateringOrderStatus) -> None:
|
||||
if self.order.status == CateringOrderStatus.CANCELED:
|
||||
return
|
||||
|
||||
if new_status == CateringOrderStatus.CANCELED:
|
||||
# ToDo: Hier sollten wir nochmal nachfragen ob der Bediener sich wirklich sicher ist,
|
||||
# und anwarnen das eine stornierte Bestellung nicht ent-storniert werden kann.
|
||||
pass
|
||||
|
||||
if self.order.status != new_status:
|
||||
if new_status == CateringOrderStatus.CANCELED:
|
||||
success = await self.session[CateringService].cancel_order(self.order)
|
||||
else:
|
||||
success = await self.session[CateringService].update_order_status(self.order.order_id, new_status)
|
||||
|
||||
if success:
|
||||
self.order = CateringOrder(
|
||||
order_id=self.order.order_id,
|
||||
order_date=self.order.order_date,
|
||||
status=new_status,
|
||||
items=self.order.items,
|
||||
customer=self.order.customer,
|
||||
is_delivery=self.order.is_delivery
|
||||
)
|
||||
|
||||
def build(self) -> Component:
|
||||
return PointerEventListener(
|
||||
content=Card(
|
||||
content=Column(
|
||||
Row(
|
||||
Text(f"ID: {self.order.order_id}", margin_left=0.3, margin_top=0.2, justify="center", style=TextStyle(font_size=1.2)),
|
||||
),
|
||||
Row(
|
||||
Text(f"Status: ", margin_left=0.3, margin_top=0.2),
|
||||
self.format_order_status(self.order.status),
|
||||
Spacer(),
|
||||
Text(self.order.order_date.strftime("%d.%m. - %H:%M Uhr"), margin_right=0.3),
|
||||
),
|
||||
Row(
|
||||
Text(f"Gast: {self.order.customer.user_name}", margin_left=0.3),
|
||||
Spacer(),
|
||||
Text(f"Sitzplatz: {'-' if not self.seat else self.seat.seat_id}", margin_right=0.3),
|
||||
),
|
||||
Row(
|
||||
Text("Diese Bestellung wird:", margin_left=0.3, margin_bottom=0.5),
|
||||
Spacer(),
|
||||
Text("Geliefert" if self.order.is_delivery else "Abgeholt", margin_right=0.3, margin_bottom=0.5),
|
||||
),
|
||||
Row(
|
||||
CateringManagementOrderDisplayStatusButton(CateringOrderStatus.RECEIVED, self.status_button_clicked),
|
||||
CateringManagementOrderDisplayStatusButton(CateringOrderStatus.CANCELED, self.status_button_clicked),
|
||||
CateringManagementOrderDisplayStatusButton(CateringOrderStatus.EN_ROUTE, self.status_button_clicked)
|
||||
),
|
||||
Row(
|
||||
CateringManagementOrderDisplayStatusButton(CateringOrderStatus.READY_FOR_PICKUP, self.status_button_clicked),
|
||||
CateringManagementOrderDisplayStatusButton(CateringOrderStatus.COMPLETED, self.status_button_clicked),
|
||||
CateringManagementOrderDisplayStatusButton(CateringOrderStatus.DELAYED, self.status_button_clicked),
|
||||
)
|
||||
),
|
||||
color=self.session.theme.hud_color,
|
||||
colorize_on_hover=True,
|
||||
margin=1
|
||||
),
|
||||
on_press=partial(self.clicked_cb, self.order)
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
from typing import Callable
|
||||
|
||||
from rio import Component, Row, Text, TextStyle, Color, Rectangle, CursorStyle
|
||||
from rio.components.pointer_event_listener import PointerEvent, PointerEventListener
|
||||
|
||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrderStatus, CateringOrder
|
||||
|
||||
MAX_LEN = 24
|
||||
|
||||
class CateringOrderItem(Component):
|
||||
order: CateringOrder
|
||||
info_modal_cb: Callable
|
||||
|
||||
def get_display_text_and_color_for_order_status(self, order_status: CateringOrderStatus) -> tuple[str, Color]:
|
||||
match order_status:
|
||||
case CateringOrderStatus.RECEIVED:
|
||||
return "In Bearbeitung", self.session.theme.success_color
|
||||
case CateringOrderStatus.DELAYED:
|
||||
return "Verspätet", Color.from_hex("eed202")
|
||||
case CateringOrderStatus.READY_FOR_PICKUP:
|
||||
return "Abholbereit", self.session.theme.success_color
|
||||
case CateringOrderStatus.EN_ROUTE:
|
||||
return "Unterwegs", self.session.theme.success_color
|
||||
case CateringOrderStatus.COMPLETED:
|
||||
return "Abgeschlossen", self.session.theme.success_color
|
||||
case CateringOrderStatus.CANCELED:
|
||||
return "Storniert", self.session.theme.danger_color
|
||||
case _:
|
||||
return "Unbekannt(wtf?)", self.session.theme.danger_color
|
||||
|
||||
|
||||
def build(self) -> Component:
|
||||
order_status, color = self.get_display_text_and_color_for_order_status(self.order.status)
|
||||
return PointerEventListener(
|
||||
Rectangle(
|
||||
content=Row(
|
||||
Text(f"ID: {str(self.order.order_id):0>6}", align_x=0, overflow="wrap", min_width=10, style=TextStyle(fill=self.session.theme.background_color, font_size=0.9), margin_right=1),
|
||||
Text(order_status, overflow="wrap", min_width=10, style=TextStyle(fill=color, font_size=0.9), margin_right=1),
|
||||
Text(self.order.order_date.strftime("%d.%m. %H:%M"), style=TextStyle(fill=self.session.theme.background_color, font_size=0.9), align_x=1)
|
||||
),
|
||||
fill=self.session.theme.primary_color,
|
||||
hover_fill=self.session.theme.hud_color,
|
||||
transition_time=0.1,
|
||||
cursor=CursorStyle.POINTER
|
||||
),
|
||||
on_press=lambda _: self.info_modal_cb(self.order),
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
from decimal import Decimal
|
||||
from typing import Callable
|
||||
|
||||
import rio
|
||||
from rio import Component, Row, Text, IconButton, TextStyle, Column, Spacer, Card, Color
|
||||
|
||||
from src.ezgg_lan_manager import AccountingService
|
||||
|
||||
MAX_LEN = 24
|
||||
|
||||
|
||||
class CateringSelectionItem(Component):
|
||||
article_name: str
|
||||
article_price: Decimal
|
||||
article_id: int
|
||||
on_add_callback: Callable
|
||||
is_sensitive: bool
|
||||
additional_info: str
|
||||
is_grey: bool
|
||||
|
||||
@staticmethod
|
||||
def split_article_name(article_name: str) -> tuple[str, str]:
|
||||
if len(article_name) <= MAX_LEN:
|
||||
return article_name, ""
|
||||
top, bottom = "", ""
|
||||
words = article_name.split(" ")
|
||||
last_word_added = ""
|
||||
while len(top) <= MAX_LEN:
|
||||
w = words.pop(0)
|
||||
top += f" {w}"
|
||||
last_word_added = w
|
||||
|
||||
top = top.replace(last_word_added, "")
|
||||
bottom = f"{last_word_added} " + " ".join(words)
|
||||
|
||||
return top.strip(), bottom.strip()
|
||||
|
||||
def build(self) -> rio.Component:
|
||||
article_name_top, article_name_bottom = self.split_article_name(self.article_name)
|
||||
|
||||
return Card(
|
||||
content=Column(
|
||||
Row(
|
||||
Text(article_name_top, align_x=0, overflow="wrap", min_width=19,
|
||||
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
|
||||
Text(AccountingService.make_euro_string_from_decimal(self.article_price),
|
||||
style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
|
||||
IconButton(
|
||||
icon="material/add",
|
||||
min_size=2,
|
||||
color=self.session.theme.success_color,
|
||||
style="plain-text",
|
||||
on_press=lambda: self.on_add_callback(self.article_id),
|
||||
is_sensitive=self.is_sensitive
|
||||
),
|
||||
proportions=(19, 5, 2),
|
||||
margin_bottom=0
|
||||
),
|
||||
Spacer() if not article_name_bottom else Text(article_name_bottom, align_x=0, overflow="wrap",
|
||||
min_width=19,
|
||||
style=TextStyle(fill=self.session.theme.background_color,
|
||||
font_size=0.9)),
|
||||
Row(
|
||||
Text(
|
||||
self.additional_info,
|
||||
align_x=0,
|
||||
overflow="wrap",
|
||||
min_width=19,
|
||||
style=TextStyle(fill=self.session.theme.background_color, font_size=0.6)
|
||||
),
|
||||
margin_top=0
|
||||
),
|
||||
margin_bottom=0.5,
|
||||
),
|
||||
color=Color.from_hex("d3d3d3") if self.is_grey else self.session.theme.primary_color
|
||||
)
|
||||
@@ -0,0 +1,93 @@
|
||||
from copy import copy, deepcopy
|
||||
from typing import Optional, Callable
|
||||
|
||||
from rio import *
|
||||
|
||||
from src.ezgg_lan_manager import ConfigurationService, UserService, LocalDataService
|
||||
from src.ezgg_lan_manager.components.DesktopNavigationButton import DesktopNavigationButton
|
||||
from src.ezgg_lan_manager.components.UserInfoAndLoginBox import UserInfoAndLoginBox
|
||||
from src.ezgg_lan_manager.services.LocalDataService import LocalData
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
|
||||
class DesktopNavigation(Component):
|
||||
user: Optional[User] = None
|
||||
force_login_box_refresh: list[Callable] = []
|
||||
|
||||
@event.on_populate
|
||||
async def async_init(self) -> None:
|
||||
self.session[SessionStorage].subscribe_to_logged_in_or_out_event(str(self.__class__), self.async_init)
|
||||
local_data = self.session[LocalData]
|
||||
if local_data.stored_session_token:
|
||||
session_ = self.session[LocalDataService].verify_token(local_data.stored_session_token)
|
||||
if session_:
|
||||
self.session.detach(SessionStorage)
|
||||
self.session.attach(session_)
|
||||
self.user = await self.session[UserService].get_user(session_.user_id)
|
||||
try:
|
||||
# Hack-around, maybe fix in the future
|
||||
self.force_login_box_refresh[-1]()
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
return
|
||||
|
||||
if self.session[SessionStorage].user_id:
|
||||
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||
else:
|
||||
self.user = None
|
||||
|
||||
def build(self) -> Component:
|
||||
lan_info = self.session[ConfigurationService].get_lan_info()
|
||||
user_info_and_login_box = UserInfoAndLoginBox()
|
||||
self.force_login_box_refresh.append(user_info_and_login_box.force_refresh)
|
||||
user_navigation = [
|
||||
DesktopNavigationButton("News", "./news"),
|
||||
Spacer(min_height=1),
|
||||
DesktopNavigationButton(f"Über {lan_info.name} {lan_info.iteration}", "./overview"),
|
||||
DesktopNavigationButton("Ticket kaufen", "./buy_ticket"),
|
||||
DesktopNavigationButton("Sitzplan", "./seating"),
|
||||
DesktopNavigationButton("Catering", "./catering"),
|
||||
DesktopNavigationButton("Teilnehmer", "./guests"),
|
||||
DesktopNavigationButton("Turniere", "./tournaments"),
|
||||
DesktopNavigationButton("FAQ", "./faq"),
|
||||
DesktopNavigationButton("Regeln & AGB", "./rules-gtc"),
|
||||
Spacer(min_height=1),
|
||||
DesktopNavigationButton("Discord", "https://discord.gg/8gTjg34yyH", open_new_tab=True),
|
||||
DesktopNavigationButton("Die EZ GG e.V.", "https://ezgg-ev.de/about", open_new_tab=True),
|
||||
DesktopNavigationButton("Kontakt", "./contact"),
|
||||
DesktopNavigationButton("Impressum & DSGVO", "./imprint"),
|
||||
Spacer(min_height=1)
|
||||
]
|
||||
team_navigation = [
|
||||
Text("Verwaltung", align_x=0.5, margin_top=0.3, style=TextStyle(fill=Color.from_hex("F0EADE"), font_size=1.2)),
|
||||
Text("Vorsichtig sein!", align_x=0.5, margin_top=0.3, style=TextStyle(fill=self.session.theme.danger_color, font_size=0.6)),
|
||||
DesktopNavigationButton("News", "./manage-news", is_team_navigation=True),
|
||||
DesktopNavigationButton("Benutzer", "./manage-users", is_team_navigation=True),
|
||||
DesktopNavigationButton("Catering", "./manage-catering", is_team_navigation=True),
|
||||
DesktopNavigationButton("Turniere", "./manage-tournaments", is_team_navigation=True),
|
||||
Spacer(min_height=1),
|
||||
Revealer(
|
||||
header="Normale Navigation",
|
||||
content=Column(*user_navigation),
|
||||
header_style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9)
|
||||
)
|
||||
] if self.user is not None and self.user.is_team_member else []
|
||||
|
||||
nav_to_use = copy(team_navigation) if self.user is not None and self.user.is_team_member else copy(user_navigation)
|
||||
|
||||
return Card(
|
||||
Column(
|
||||
Text(lan_info.name, align_x=0.5, margin_top=0.3, style=TextStyle(fill=self.session.theme.hud_color, font_size=1.9)),
|
||||
Text(f"Edition {lan_info.iteration}", align_x=0.5, style=TextStyle(fill=self.session.theme.hud_color, font_size=1.2), margin_top=0.3, margin_bottom=2),
|
||||
user_info_and_login_box,
|
||||
*nav_to_use,
|
||||
align_y=0
|
||||
),
|
||||
color=self.session.theme.neutral_color,
|
||||
min_width=15,
|
||||
grow_y=True,
|
||||
corner_radius=(0.5, 0, 0, 0),
|
||||
margin_right=0.1
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
from rio import Component, TextStyle, Color, Link, Button, Text
|
||||
|
||||
class DesktopNavigationButton(Component):
|
||||
STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
|
||||
TEAM_STYLE = TextStyle(fill=Color.from_hex("F0EADE"), font_size=0.9)
|
||||
label: str
|
||||
target_url: str
|
||||
is_team_navigation: bool = False
|
||||
open_new_tab: bool = False
|
||||
|
||||
def build(self) -> Component:
|
||||
return Link(
|
||||
content=Button(
|
||||
content=Text(self.label, style=self.TEAM_STYLE if self.is_team_navigation else self.STYLE),
|
||||
shape="rectangle",
|
||||
style="minor",
|
||||
color="danger" if self.is_team_navigation else "secondary",
|
||||
grow_x=True,
|
||||
margin_left=0.6,
|
||||
margin_right=0.6,
|
||||
margin_top=0.6
|
||||
),
|
||||
target_url=self.target_url,
|
||||
open_in_new_tab=self.open_new_tab
|
||||
)
|
||||
@@ -0,0 +1,106 @@
|
||||
from rio import Component, TextStyle, Color, TextInput, Button, Text, Rectangle, Column, Row, Spacer, \
|
||||
EventHandler
|
||||
|
||||
from src.ezgg_lan_manager.services.LocalDataService import LocalDataService, LocalData
|
||||
from src.ezgg_lan_manager.services.UserService import UserService
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
|
||||
class LoginBox(Component):
|
||||
status_change_cb: EventHandler = None
|
||||
user_name_input_text: str = ""
|
||||
password_input_text: str = ""
|
||||
user_name_input_is_valid = True
|
||||
password_input_is_valid = True
|
||||
login_button_is_loading = False
|
||||
is_account_locked: bool = False
|
||||
|
||||
async def _on_login_pressed(self) -> None:
|
||||
if await self.session[UserService].is_login_valid(self.user_name_input_text, self.password_input_text):
|
||||
user: User = await self.session[UserService].get_user(self.user_name_input_text)
|
||||
if not user.is_active:
|
||||
self.is_account_locked = True
|
||||
return
|
||||
self.user_name_input_is_valid = True
|
||||
self.password_input_is_valid = True
|
||||
self.login_button_is_loading = False
|
||||
self.is_account_locked = False
|
||||
await self.session[SessionStorage].set_user_id_and_team_member_flag(user.user_id, user.is_team_member)
|
||||
token = self.session[LocalDataService].set_session(self.session[SessionStorage])
|
||||
self.session[LocalData].stored_session_token = token
|
||||
self.session.attach(self.session[LocalData])
|
||||
self.status_change_cb()
|
||||
else:
|
||||
self.user_name_input_is_valid = False
|
||||
self.password_input_is_valid = False
|
||||
self.login_button_is_loading = False
|
||||
self.is_account_locked = False
|
||||
self.force_refresh()
|
||||
|
||||
def build(self) -> Component:
|
||||
user_name_input = TextInput(
|
||||
text=self.bind().user_name_input_text,
|
||||
label="Benutzername",
|
||||
accessibility_label="Benutzername",
|
||||
min_height=0.5,
|
||||
on_confirm=lambda _: self._on_login_pressed(),
|
||||
is_valid=self.user_name_input_is_valid
|
||||
)
|
||||
password_input = TextInput(
|
||||
text=self.bind().password_input_text,
|
||||
label="Passwort",
|
||||
accessibility_label="Passwort",
|
||||
is_secret=True,
|
||||
on_confirm=lambda _: self._on_login_pressed(),
|
||||
is_valid=self.password_input_is_valid
|
||||
)
|
||||
login_button = Button(
|
||||
Text("LOGIN", fill=Color.from_hex("02dac5"), style=TextStyle(font_size=0.9), justify="center"),
|
||||
shape="rectangle",
|
||||
style="minor",
|
||||
color="secondary",
|
||||
margin_bottom=0.4,
|
||||
on_press=self._on_login_pressed
|
||||
)
|
||||
register_button = Button(
|
||||
Text("REG", fill=Color.from_hex("02dac5"), style=TextStyle(font_size=0.9), justify="center"),
|
||||
shape="rectangle",
|
||||
style="minor",
|
||||
color="secondary",
|
||||
on_press=lambda: self.session.navigate_to("./register")
|
||||
)
|
||||
forgot_password_button = Button(
|
||||
Text("LST PWD", fill=Color.from_hex("02dac5"), style=TextStyle(font_size=0.9), justify="center"),
|
||||
shape="rectangle",
|
||||
style="minor",
|
||||
color="secondary",
|
||||
on_press=lambda: self.session.navigate_to("./forgot-password")
|
||||
)
|
||||
|
||||
return Rectangle(
|
||||
content=Column(
|
||||
user_name_input,
|
||||
password_input,
|
||||
Column(
|
||||
Row(
|
||||
login_button
|
||||
),
|
||||
Row(
|
||||
register_button,
|
||||
Spacer(),
|
||||
forgot_password_button,
|
||||
proportions=(49, 2, 49)
|
||||
),
|
||||
margin_bottom=0.5
|
||||
),
|
||||
Text(text="Dieses Konto\nist gesperrt", fill=self.session.theme.danger_color, style=TextStyle(font_size=0.9 if self.is_account_locked else 0), align_x=0.5),
|
||||
spacing=0.4
|
||||
),
|
||||
fill=Color.TRANSPARENT,
|
||||
min_height=8,
|
||||
min_width=12,
|
||||
align_x=0.5,
|
||||
margin_top=0.3,
|
||||
margin_bottom=2
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
from typing import Optional
|
||||
|
||||
from rio import Component, Rectangle, Text
|
||||
|
||||
|
||||
class MainViewContentBox(Component):
|
||||
content: Optional[Component] = None
|
||||
|
||||
def build(self) -> Component:
|
||||
if self.content is None:
|
||||
content = Text("Vielleich sollte hier etwas sein...\n\n\n... Wenn ja, habe ich es nicht gefunden. :(")
|
||||
else:
|
||||
content = self.content
|
||||
return Rectangle(
|
||||
content=content,
|
||||
fill=self.session.theme.primary_color,
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
margin_top=1,
|
||||
margin_bottom=1,
|
||||
shadow_radius=0.5,
|
||||
shadow_color=self.session.theme.hud_color,
|
||||
shadow_offset_y=0,
|
||||
corner_radius=0.2
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
|
||||
from rio import Component, Column, NumberInput, ThemeContextSwitcher, TextInput, Row, Button, EventHandler
|
||||
|
||||
from src.ezgg_lan_manager.types.Transaction import Transaction
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
|
||||
class NewTransactionForm(Component):
|
||||
user: Optional[User] = None
|
||||
input_value: float = 0
|
||||
input_reason: str = ""
|
||||
new_transaction_cb: EventHandler[Transaction] = None
|
||||
|
||||
async def send_debit_transaction(self) -> None:
|
||||
await self.call_event_handler(
|
||||
self.new_transaction_cb,
|
||||
Transaction(
|
||||
user_id=self.user.user_id,
|
||||
value=Decimal(str(self.input_value)),
|
||||
is_debit=True,
|
||||
reference=self.input_reason,
|
||||
transaction_date=datetime.now()
|
||||
)
|
||||
)
|
||||
|
||||
async def send_credit_transaction(self) -> None:
|
||||
await self.call_event_handler(
|
||||
self.new_transaction_cb,
|
||||
Transaction(
|
||||
user_id=self.user.user_id,
|
||||
value=Decimal(str(self.input_value)),
|
||||
is_debit=False,
|
||||
reference=self.input_reason,
|
||||
transaction_date=datetime.now()
|
||||
)
|
||||
)
|
||||
|
||||
def build(self) -> Component:
|
||||
return ThemeContextSwitcher(
|
||||
content=Column(
|
||||
NumberInput(
|
||||
value=self.bind().input_value,
|
||||
label="Betrag",
|
||||
suffix_text="€",
|
||||
decimals=2,
|
||||
thousands_separator=".",
|
||||
margin=1,
|
||||
margin_bottom=0
|
||||
),
|
||||
TextInput(
|
||||
text=self.bind().input_reason,
|
||||
label="Beschreibung",
|
||||
margin=1,
|
||||
margin_bottom=0
|
||||
),
|
||||
Row(
|
||||
Button(
|
||||
content="Entfernen",
|
||||
shape="rectangle",
|
||||
color="danger",
|
||||
margin=1,
|
||||
on_press=self.send_debit_transaction
|
||||
),
|
||||
Button(
|
||||
content="Hinzufügen",
|
||||
shape="rectangle",
|
||||
color="success",
|
||||
margin=1,
|
||||
on_press=self.send_credit_transaction
|
||||
)
|
||||
)
|
||||
),
|
||||
color="primary"
|
||||
)
|
||||
@@ -0,0 +1,150 @@
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from typing import Optional, Callable
|
||||
|
||||
from rio import Component, Rectangle, Text, TextStyle, Column, Row, TextInput, DateInput, MultiLineTextInput, IconButton, Color, Button, ThemeContextSwitcher
|
||||
|
||||
|
||||
class NewsPost(Component):
|
||||
title: str = ""
|
||||
text: str = ""
|
||||
date: str = ""
|
||||
subtitle: str = ""
|
||||
author: str = ""
|
||||
|
||||
def build(self) -> Component:
|
||||
return Rectangle(
|
||||
content=Column(
|
||||
Row(
|
||||
Text(
|
||||
self.title,
|
||||
grow_x=True,
|
||||
margin=2,
|
||||
margin_bottom=0,
|
||||
fill=self.session.theme.background_color,
|
||||
style=TextStyle(
|
||||
font_size=1.3
|
||||
),
|
||||
overflow="ellipsize"
|
||||
),
|
||||
Text(
|
||||
self.date,
|
||||
margin=2,
|
||||
align_x=1,
|
||||
fill=self.session.theme.background_color,
|
||||
style=TextStyle(
|
||||
font_size=0.6
|
||||
),
|
||||
overflow="wrap"
|
||||
)
|
||||
),
|
||||
Text(
|
||||
self.subtitle,
|
||||
grow_x=True,
|
||||
margin=2,
|
||||
margin_top=0,
|
||||
margin_bottom=0,
|
||||
fill=self.session.theme.background_color,
|
||||
style=TextStyle(
|
||||
font_size=0.8
|
||||
),
|
||||
overflow="ellipsize"
|
||||
),
|
||||
Text(
|
||||
self.text,
|
||||
margin=2,
|
||||
fill=self.session.theme.background_color,
|
||||
overflow="wrap"
|
||||
),
|
||||
Text(
|
||||
f"Geschrieben von {self.author}",
|
||||
align_x=0,
|
||||
grow_x=True,
|
||||
margin=2,
|
||||
margin_top=0,
|
||||
margin_bottom=1,
|
||||
fill=self.session.theme.background_color,
|
||||
style=TextStyle(
|
||||
font_size=0.5,
|
||||
italic=True
|
||||
),
|
||||
overflow="nowrap"
|
||||
)
|
||||
),
|
||||
fill=self.session.theme.primary_color,
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
margin_top=2,
|
||||
margin_bottom=1,
|
||||
shadow_radius=0.5,
|
||||
shadow_color=self.session.theme.hud_color,
|
||||
shadow_offset_y=0,
|
||||
corner_radius=0.2
|
||||
)
|
||||
|
||||
|
||||
class EditableNewsPost(NewsPost):
|
||||
news_id: int = -1
|
||||
save_cb: Callable = lambda _: None
|
||||
delete_cb: Callable = lambda _: None
|
||||
|
||||
def set_prop(self, prop, value) -> None:
|
||||
self.__setattr__(prop, value)
|
||||
|
||||
def build(self) -> Component:
|
||||
return ThemeContextSwitcher(
|
||||
content=Rectangle(
|
||||
content=Column(
|
||||
Row(
|
||||
TextInput(
|
||||
text=self.title,
|
||||
label="Titel",
|
||||
style="rounded",
|
||||
min_width=15,
|
||||
on_change=lambda e: self.set_prop("title", e.text)
|
||||
),
|
||||
DateInput(
|
||||
value=datetime.strptime(self.date, "%d.%m.%Y"),
|
||||
style="rounded",
|
||||
on_change=lambda e: self.set_prop("date", e.value.strftime("%d.%m.%Y"))
|
||||
)
|
||||
),
|
||||
TextInput(
|
||||
text=self.subtitle,
|
||||
label="Untertitel",
|
||||
style="rounded",
|
||||
grow_x=True,
|
||||
on_change=lambda e: self.set_prop("subtitle", e.text)
|
||||
),
|
||||
MultiLineTextInput(
|
||||
text=self.text,
|
||||
label="Text",
|
||||
style="rounded",
|
||||
grow_x=True,
|
||||
min_height=12,
|
||||
on_change=lambda e: self.set_prop("text", e.text)
|
||||
),
|
||||
Row(
|
||||
TextInput(
|
||||
text=self.author,
|
||||
label="Autor",
|
||||
style="rounded",
|
||||
grow_x=True,
|
||||
on_change=lambda e: self.set_prop("author", e.text)
|
||||
),
|
||||
Rectangle(content=Button(icon="material/delete", style="major", color="danger", shape="rectangle", on_press=partial(self.delete_cb, self.news_id)), fill=Color.from_hex("0b7372")),
|
||||
Rectangle(content=Button(icon="material/save", style="major", color="success", shape="rectangle", on_press=partial(self.save_cb, self)), fill=Color.from_hex("0b7372"))
|
||||
)
|
||||
),
|
||||
fill=self.session.theme.primary_color,
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
margin_top=2,
|
||||
margin_bottom=1,
|
||||
shadow_radius=0.2,
|
||||
shadow_color=self.session.theme.background_color,
|
||||
shadow_offset_y=0,
|
||||
corner_radius=0.2
|
||||
),
|
||||
color="primary"
|
||||
)
|
||||
@@ -0,0 +1,274 @@
|
||||
from typing import Callable
|
||||
|
||||
from rio import Component, Rectangle, Grid, Column, Row, Text, TextStyle, Color, PointerEventListener, Spacer
|
||||
|
||||
from src.ezgg_lan_manager.components.SeatingPlanPixels import SeatPixel, WallPixel, InvisiblePixel, TextPixel
|
||||
from src.ezgg_lan_manager.types.Seat import Seat
|
||||
|
||||
MAX_GRID_WIDTH_PIXELS = 60
|
||||
MAX_GRID_HEIGHT_PIXELS = 60
|
||||
|
||||
class SeatingPlanLegend(Component):
|
||||
def build(self) -> Component:
|
||||
return Column(
|
||||
Text("Legende", style=TextStyle(fill=self.session.theme.neutral_color), justify="center", margin=1),
|
||||
Row( # Disabled for upcoming LAN
|
||||
Spacer(),
|
||||
Rectangle(
|
||||
content=Text("Normaler Platz", style=TextStyle(fill=self.session.theme.neutral_color, font_size=0.7), margin=0.2, justify="center"),
|
||||
fill=Color.TRANSPARENT,
|
||||
stroke_width=0.2,
|
||||
stroke_color=Color.from_hex("003300"),
|
||||
min_width=20,
|
||||
margin_right=1
|
||||
),
|
||||
Rectangle(
|
||||
content=Text("Deluxe Platz", style=TextStyle(fill=self.session.theme.neutral_color, font_size=0.7), margin=0.2, justify="center"),
|
||||
fill=Color.TRANSPARENT,
|
||||
stroke_width=0.2,
|
||||
stroke_color=Color.from_hex("66ff99"),
|
||||
min_width=20
|
||||
),
|
||||
Spacer()
|
||||
),
|
||||
Row(
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Text(f"Freier Platz", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.7), align_x=0.5, selectable=False),
|
||||
Text(f"", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9), align_x=0.5,
|
||||
selectable=False, overflow="wrap")
|
||||
),
|
||||
min_width=1,
|
||||
min_height=1,
|
||||
fill=self.session.theme.success_color,
|
||||
grow_x=False,
|
||||
grow_y=False,
|
||||
hover_fill=self.session.theme.success_color,
|
||||
transition_time=0.4,
|
||||
ripple=True
|
||||
),
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Text(f"Belegter Platz", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.7), align_x=0.5, selectable=False),
|
||||
Text(f"", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9), align_x=0.5,
|
||||
selectable=False, overflow="wrap")
|
||||
),
|
||||
min_width=1,
|
||||
min_height=1,
|
||||
fill=self.session.theme.danger_color,
|
||||
grow_x=False,
|
||||
grow_y=False,
|
||||
hover_fill=self.session.theme.danger_color,
|
||||
transition_time=0.4,
|
||||
ripple=True
|
||||
),
|
||||
Rectangle(
|
||||
content=Column(
|
||||
Text(f"Eigener Platz", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.7), align_x=0.5, selectable=False),
|
||||
Text(f"", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9), align_x=0.5,
|
||||
selectable=False, overflow="wrap")
|
||||
),
|
||||
min_width=1,
|
||||
min_height=1,
|
||||
fill=Color.from_hex("800080"),
|
||||
grow_x=False,
|
||||
grow_y=False,
|
||||
hover_fill=Color.from_hex("800080"),
|
||||
transition_time=0.4,
|
||||
ripple=True
|
||||
),
|
||||
margin=1,
|
||||
spacing=1
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class SeatingPlan(Component):
|
||||
seat_clicked_cb: Callable
|
||||
seating_info: list[Seat]
|
||||
info_clicked_cb: Callable
|
||||
|
||||
def get_seat(self, seat_id: str) -> Seat:
|
||||
seat = next(filter(lambda seat_: seat_.seat_id == seat_id, self.seating_info), None)
|
||||
return seat if seat else Seat(seat_id="Z99", is_blocked=True, category="LUXUS", user=None)
|
||||
|
||||
"""
|
||||
This seating plan is for the community center "Bottenhorn"
|
||||
"""
|
||||
def build(self) -> Component:
|
||||
grid = Grid()
|
||||
# Outlines
|
||||
for x in range(0, MAX_GRID_WIDTH_PIXELS):
|
||||
grid.add(WallPixel(), row=0, column=x)
|
||||
|
||||
for y in range(0, MAX_GRID_HEIGHT_PIXELS):
|
||||
grid.add(WallPixel(), row=y, column=0)
|
||||
|
||||
for x in range(0, MAX_GRID_WIDTH_PIXELS):
|
||||
grid.add(WallPixel(), row=MAX_GRID_HEIGHT_PIXELS, column=x)
|
||||
|
||||
for x in range(0, 31):
|
||||
grid.add(WallPixel(), row=15, column=x)
|
||||
|
||||
for x in range(41, MAX_GRID_WIDTH_PIXELS):
|
||||
grid.add(WallPixel(), row=32, column=x)
|
||||
|
||||
for x in range(31, 34):
|
||||
grid.add(WallPixel(), row=32, column=x)
|
||||
grid.add(WallPixel(), row=19, column=x)
|
||||
|
||||
for x in range(42, MAX_GRID_WIDTH_PIXELS):
|
||||
grid.add(WallPixel(), row=11, column=x)
|
||||
grid.add(WallPixel(), row=22, column=x)
|
||||
|
||||
for x in range(22, 30):
|
||||
grid.add(WallPixel(), row=5, column=x)
|
||||
grid.add(WallPixel(), row=10, column=x)
|
||||
|
||||
for y in range(5, 11):
|
||||
grid.add(WallPixel(), row=y, column=21)
|
||||
grid.add(WallPixel(), row=y, column=30)
|
||||
|
||||
|
||||
for y in range(40, MAX_GRID_HEIGHT_PIXELS):
|
||||
grid.add(WallPixel(), row=y, column=30)
|
||||
|
||||
for y in range(32, 36):
|
||||
grid.add(WallPixel(), row=y, column=30)
|
||||
|
||||
for y in range(19, 33):
|
||||
grid.add(WallPixel(), row=y, column=34)
|
||||
|
||||
for y in range(16, 20):
|
||||
grid.add(WallPixel(), row=y, column=30)
|
||||
|
||||
for y in range(0, 5):
|
||||
grid.add(WallPixel(), row=y, column=41)
|
||||
|
||||
for y in range(9, 15):
|
||||
grid.add(WallPixel(), row=y, column=41)
|
||||
|
||||
for y in range(19, 33):
|
||||
grid.add(WallPixel(), row=y, column=41)
|
||||
|
||||
|
||||
# Block A
|
||||
grid.add(SeatPixel("A01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A01")), row=57, column=1, width=5, height=2)
|
||||
grid.add(SeatPixel("A02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A02")), row=57, column=6, width=5, height=2)
|
||||
grid.add(SeatPixel("A03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A03")), row=57, column=11, width=5, height=2)
|
||||
grid.add(SeatPixel("A04", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A04")), row=57, column=16, width=5, height=2)
|
||||
grid.add(SeatPixel("A05", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A05")), row=57, column=21, width=5, height=2)
|
||||
|
||||
grid.add(SeatPixel("A10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A10")), row=55, column=1, width=5, height=2)
|
||||
grid.add(SeatPixel("A11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A11")), row=55, column=6, width=5, height=2)
|
||||
grid.add(SeatPixel("A12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A12")), row=55, column=11, width=5, height=2)
|
||||
grid.add(SeatPixel("A13", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A13")), row=55, column=16, width=5, height=2)
|
||||
grid.add(SeatPixel("A14", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("A14")), row=55, column=21, width=5, height=2)
|
||||
|
||||
# Block B
|
||||
grid.add(SeatPixel("B01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B01")), row=50, column=1, width=3, height=2)
|
||||
grid.add(SeatPixel("B02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B02")), row=50, column=4, width=3, height=2)
|
||||
grid.add(SeatPixel("B03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B03")), row=50, column=7, width=3, height=2)
|
||||
grid.add(SeatPixel("B04", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B04")), row=50, column=10, width=3, height=2)
|
||||
grid.add(SeatPixel("B05", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B05")), row=50, column=13, width=3, height=2)
|
||||
grid.add(SeatPixel("B06", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B06")), row=50, column=16, width=3, height=2)
|
||||
|
||||
grid.add(SeatPixel("B10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B10")), row=48, column=1, width=3, height=2)
|
||||
grid.add(SeatPixel("B11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B11")), row=48, column=4, width=3, height=2)
|
||||
grid.add(SeatPixel("B12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B12")), row=48, column=7, width=3, height=2)
|
||||
grid.add(SeatPixel("B13", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B13")), row=48, column=10, width=3, height=2)
|
||||
grid.add(SeatPixel("B14", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B14")), row=48, column=13, width=3, height=2)
|
||||
grid.add(SeatPixel("B15", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("B15")), row=48, column=16, width=3, height=2)
|
||||
|
||||
# Block C
|
||||
grid.add(SeatPixel("C01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C01")), row=43, column=1, width=3, height=2)
|
||||
grid.add(SeatPixel("C02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C02")), row=43, column=4, width=3, height=2)
|
||||
grid.add(SeatPixel("C03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C03")), row=43, column=7, width=3, height=2)
|
||||
grid.add(SeatPixel("C04", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C04")), row=43, column=10, width=3, height=2)
|
||||
grid.add(SeatPixel("C05", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C05")), row=43, column=13, width=3, height=2)
|
||||
grid.add(SeatPixel("C06", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C06")), row=43, column=16, width=3, height=2)
|
||||
|
||||
grid.add(SeatPixel("C10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C10")), row=41, column=1, width=3, height=2)
|
||||
grid.add(SeatPixel("C11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C11")), row=41, column=4, width=3, height=2)
|
||||
grid.add(SeatPixel("C12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C12")), row=41, column=7, width=3, height=2)
|
||||
grid.add(SeatPixel("C13", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C13")), row=41, column=10, width=3, height=2)
|
||||
grid.add(SeatPixel("C14", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C14")), row=41, column=13, width=3, height=2)
|
||||
grid.add(SeatPixel("C15", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("C15")), row=41, column=16, width=3, height=2)
|
||||
|
||||
# Block D
|
||||
grid.add(SeatPixel("D01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D01")), row=34, column=1, width=5, height=2)
|
||||
grid.add(SeatPixel("D02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D02")), row=34, column=6, width=5, height=2)
|
||||
grid.add(SeatPixel("D03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D03")), row=34, column=11, width=5, height=2)
|
||||
grid.add(SeatPixel("D04", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D04")), row=34, column=16, width=5, height=2)
|
||||
grid.add(SeatPixel("D05", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D05")), row=34, column=21, width=5, height=2)
|
||||
|
||||
grid.add(SeatPixel("D10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D10")), row=32, column=1, width=5, height=2)
|
||||
grid.add(SeatPixel("D11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D11")), row=32, column=6, width=5, height=2)
|
||||
grid.add(SeatPixel("D12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D12")), row=32, column=11, width=5, height=2)
|
||||
grid.add(SeatPixel("D13", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D13")), row=32, column=16, width=5, height=2)
|
||||
grid.add(SeatPixel("D14", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("D14")), row=32, column=21, width=5, height=2)
|
||||
|
||||
# Block E
|
||||
grid.add(SeatPixel("E01", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E01")), row=27, column=1, width=5, height=2)
|
||||
grid.add(SeatPixel("E02", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E02")), row=27, column=6, width=5, height=2)
|
||||
grid.add(SeatPixel("E03", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E03")), row=27, column=11, width=5, height=2)
|
||||
grid.add(SeatPixel("E04", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E04")), row=27, column=16, width=5, height=2)
|
||||
grid.add(SeatPixel("E05", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E05")), row=27, column=21, width=5, height=2)
|
||||
|
||||
grid.add(SeatPixel("E10", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E10")), row=25, column=1, width=5, height=2)
|
||||
grid.add(SeatPixel("E11", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E11")), row=25, column=6, width=5, height=2)
|
||||
grid.add(SeatPixel("E12", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E12")), row=25, column=11, width=5, height=2)
|
||||
grid.add(SeatPixel("E13", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E13")), row=25, column=16, width=5, height=2)
|
||||
grid.add(SeatPixel("E14", on_press_cb=self.seat_clicked_cb, seat=self.get_seat("E14")), row=25, column=21, width=5, height=2)
|
||||
|
||||
# Stage
|
||||
grid.add(PointerEventListener(
|
||||
TextPixel(text="Bühne"),
|
||||
on_press=lambda _: self.info_clicked_cb("Hier darf ab Freitag 20 Uhr ebenfalls geschlafen werden.")
|
||||
), row=16, column=1, width=29, height=4)
|
||||
|
||||
# Drinks
|
||||
grid.add(PointerEventListener(
|
||||
TextPixel(text="G\ne\nt\nr\nä\nn\nk\ne"),
|
||||
on_press=lambda _: self.info_clicked_cb("Ich mag Bier, B - I - R")
|
||||
), row=20, column=30, width=4, height=12)
|
||||
|
||||
# Main Entrance
|
||||
grid.add(PointerEventListener(
|
||||
TextPixel(text="H\na\nl\nl\ne\nn\n\ne\ni\nn\ng\na\nn\ng"),
|
||||
on_press=lambda _: self.info_clicked_cb("Hallo, ich bin ein Haupteingang")
|
||||
), row=33, column=56, width=4, height=27)
|
||||
|
||||
# Sleeping
|
||||
grid.add(PointerEventListener(
|
||||
TextPixel(icon_name="material/bed"),
|
||||
on_press=lambda _: self.info_clicked_cb("In diesem Raum kann geschlafen werden.\nAchtung: Hier werden nicht alle Teilnehmer Platz finden.")
|
||||
), row=1, column=1, width=20, height=14)
|
||||
|
||||
# Toilet
|
||||
grid.add(PointerEventListener(
|
||||
TextPixel(icon_name="material/wc"),
|
||||
on_press=lambda _: self.info_clicked_cb("Damen Toilette")
|
||||
), row=1, column=42, width=19, height=10)
|
||||
grid.add(PointerEventListener(
|
||||
TextPixel(icon_name="material/wc"),
|
||||
on_press=lambda _: self.info_clicked_cb("Herren Toilette")
|
||||
), row=12, column=42, width=19, height=10)
|
||||
|
||||
# Entry/Helpdesk
|
||||
grid.add(PointerEventListener(
|
||||
TextPixel(text="Einlass\n &Orga"),
|
||||
on_press=lambda _: self.info_clicked_cb("Für alle Anliegen findest du hier rund um die Uhr jemanden vom Team.")
|
||||
), row=40, column=22, width=8, height=12)
|
||||
|
||||
|
||||
return Rectangle(
|
||||
content=grid,
|
||||
grow_x=True,
|
||||
grow_y=True,
|
||||
stroke_color=self.session.theme.neutral_color,
|
||||
stroke_width=0.1,
|
||||
fill=self.session.theme.primary_color,
|
||||
margin=0.5
|
||||
)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
from decimal import Decimal
|
||||
from functools import partial
|
||||
from typing import Optional, Callable
|
||||
|
||||
from rio import Component, Column, Text, TextStyle, Button, Spacer, event
|
||||
|
||||
from src.ezgg_lan_manager import TicketingService
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
|
||||
|
||||
class SeatingPlanInfoBox(Component):
|
||||
show: bool
|
||||
purchase_cb: Callable
|
||||
is_booking_blocked: bool
|
||||
seat_id: Optional[str] = None
|
||||
seat_occupant: Optional[str] = None
|
||||
seat_price: Decimal = Decimal("0")
|
||||
is_blocked: bool = False
|
||||
has_user_ticket: bool = False
|
||||
booking_button_text: str = ""
|
||||
override_text: str = "" # If this is set, all other functionality is disabled and the text is shown
|
||||
|
||||
@event.on_populate
|
||||
async def check_ticket(self) -> None:
|
||||
if self.session[SessionStorage].user_id:
|
||||
user_ticket = await self.session[TicketingService].get_user_ticket(self.session[SessionStorage].user_id)
|
||||
self.has_user_ticket = not (user_ticket is None)
|
||||
self.booking_button_text = "Buchen" if self.has_user_ticket else "Ticket kaufen"
|
||||
self.force_refresh()
|
||||
|
||||
async def purchase_clicked(self):
|
||||
if self.has_user_ticket:
|
||||
await self.purchase_cb()
|
||||
else:
|
||||
self.session.navigate_to("./buy_ticket")
|
||||
|
||||
def build(self) -> Component:
|
||||
if self.override_text:
|
||||
return Column(Text(self.override_text, margin=1,
|
||||
style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.4), overflow="wrap",
|
||||
justify="center"), min_height=10)
|
||||
|
||||
if not self.show:
|
||||
return Spacer()
|
||||
if self.is_blocked:
|
||||
return Column(Text(f"Sitzplatz gesperrt", margin=1,
|
||||
style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.4), overflow="wrap",
|
||||
justify="center"), min_height=10)
|
||||
if self.seat_id is None and self.seat_occupant is None:
|
||||
return Column(
|
||||
Text(f"Sitzplatz auswählen...", margin=1, style=TextStyle(fill=self.session.theme.neutral_color),
|
||||
overflow="wrap", justify="center"), min_height=10)
|
||||
return Column(
|
||||
Text(f"Dieser Sitzplatz ({self.seat_id}) ist gebucht von:", margin=1,
|
||||
style=TextStyle(fill=self.session.theme.neutral_color), overflow="wrap", justify="center"),
|
||||
Text(f"{self.seat_occupant}", margin_bottom=1,
|
||||
style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.4), overflow="wrap",
|
||||
justify="center"),
|
||||
min_height=10
|
||||
) if self.seat_id and self.seat_occupant else Column(
|
||||
Text(f"Dieser Sitzplatz ({self.seat_id}) ist frei", margin=1,
|
||||
style=TextStyle(fill=self.session.theme.neutral_color), overflow="wrap", justify="center"),
|
||||
Button(
|
||||
Text(
|
||||
text=self.booking_button_text,
|
||||
margin=1,
|
||||
style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.1),
|
||||
overflow="wrap",
|
||||
justify="center"
|
||||
),
|
||||
shape="rounded",
|
||||
style="major",
|
||||
color="secondary",
|
||||
margin=1,
|
||||
grow_y=False,
|
||||
is_sensitive=not self.is_booking_blocked,
|
||||
on_press=self.purchase_clicked
|
||||
) if self.session[SessionStorage].user_id else Text(f"Du musst eingeloggt sein um einen Sitzplatz zu buchen",
|
||||
margin=1,
|
||||
style=TextStyle(fill=self.session.theme.neutral_color),
|
||||
overflow="wrap", justify="center"),
|
||||
min_height=10
|
||||
)
|
||||
@@ -0,0 +1,104 @@
|
||||
from functools import partial
|
||||
|
||||
from rio import Component, Text, Icon, TextStyle, Rectangle, Spacer, Color, PointerEventListener, Column, Row
|
||||
from typing import Optional, Callable
|
||||
|
||||
from src.ezgg_lan_manager.types.Seat import Seat
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
|
||||
|
||||
class SeatPixel(Component):
|
||||
seat_id: str
|
||||
on_press_cb: Callable
|
||||
seat: Seat
|
||||
|
||||
def determine_color(self) -> Color:
|
||||
if self.seat.user is not None and self.seat.user.user_id == self.session[SessionStorage].user_id:
|
||||
return Color.from_hex("800080")
|
||||
elif self.seat.is_blocked or self.seat.user is not None:
|
||||
return self.session.theme.danger_color
|
||||
return self.session.theme.success_color
|
||||
|
||||
def build(self) -> Component:
|
||||
return PointerEventListener(
|
||||
content=Rectangle(
|
||||
content=Row(
|
||||
Text(f"{self.seat_id}", style=TextStyle(fill=self.session.theme.primary_color, font_size=0.9), align_x=0.5, selectable=False)
|
||||
),
|
||||
min_width=1,
|
||||
min_height=1,
|
||||
fill=self.determine_color(),
|
||||
stroke_width = 0.1,
|
||||
hover_stroke_width = 0.1,
|
||||
stroke_color=Color.from_hex("003300") if self.seat.category == "NORMAL" else Color.from_hex("66ff99"),
|
||||
grow_x=True,
|
||||
grow_y=True,
|
||||
hover_fill=self.session.theme.hud_color,
|
||||
transition_time=0.4,
|
||||
ripple=True
|
||||
),
|
||||
on_press=partial(self.on_press_cb, self.seat_id)
|
||||
)
|
||||
|
||||
class TextPixel(Component):
|
||||
text: Optional[str] = None
|
||||
icon_name: Optional[str] = None
|
||||
no_outline: bool = False
|
||||
|
||||
def build(self) -> Component:
|
||||
if self.text is not None:
|
||||
content = Text(self.text, style=TextStyle(fill=self.session.theme.neutral_color, font_size=1), align_x=0.5, selectable=False)
|
||||
elif self.icon_name is not None:
|
||||
content = Icon(self.icon_name, fill=self.session.theme.neutral_color)
|
||||
else:
|
||||
content = None
|
||||
return Rectangle(
|
||||
content=content,
|
||||
min_width=1,
|
||||
min_height=1,
|
||||
fill=self.session.theme.primary_color,
|
||||
stroke_width=0.0 if self.no_outline else 0.1,
|
||||
stroke_color=self.session.theme.neutral_color,
|
||||
hover_stroke_width = None if self.no_outline else 0.1,
|
||||
grow_x=True,
|
||||
grow_y=True,
|
||||
hover_fill=None,
|
||||
ripple=True
|
||||
)
|
||||
|
||||
class WallPixel(Component):
|
||||
def build(self) -> Component:
|
||||
return Rectangle(
|
||||
min_width=1,
|
||||
min_height=1,
|
||||
fill=Color.from_hex("434343"),
|
||||
grow_x=True,
|
||||
grow_y=True,
|
||||
)
|
||||
|
||||
class DebugPixel(Component):
|
||||
def build(self) -> Component:
|
||||
return Rectangle(
|
||||
content=Spacer(),
|
||||
min_width=1,
|
||||
min_height=1,
|
||||
fill=self.session.theme.success_color,
|
||||
hover_stroke_color = self.session.theme.hud_color,
|
||||
hover_stroke_width = 0.1,
|
||||
grow_x=True,
|
||||
grow_y=True,
|
||||
hover_fill=self.session.theme.secondary_color,
|
||||
transition_time=0.1
|
||||
)
|
||||
|
||||
class InvisiblePixel(Component):
|
||||
def build(self) -> Component:
|
||||
return Rectangle(
|
||||
content=Spacer(),
|
||||
min_width=1,
|
||||
min_height=1,
|
||||
fill=self.session.theme.primary_color,
|
||||
hover_stroke_width=0.0,
|
||||
grow_x=True,
|
||||
grow_y=True
|
||||
)
|
||||
@@ -0,0 +1,96 @@
|
||||
from typing import Optional, Callable
|
||||
|
||||
from rio import Component, Column, Text, TextStyle, Button, Spacer, Row, ProgressCircle
|
||||
|
||||
|
||||
class SeatingPurchaseBox(Component):
|
||||
show: bool
|
||||
seat_id: str
|
||||
is_loading: bool
|
||||
confirm_cb: Callable
|
||||
cancel_cb: Callable
|
||||
error_msg: Optional[str] = None
|
||||
success_msg: Optional[str] = None
|
||||
|
||||
def build(self) -> Component:
|
||||
if not self.show:
|
||||
return Spacer()
|
||||
if self.is_loading:
|
||||
return Column(
|
||||
ProgressCircle(
|
||||
color="secondary",
|
||||
align_x=0.5,
|
||||
margin_top=2,
|
||||
margin_bottom=2
|
||||
),
|
||||
min_height=10
|
||||
)
|
||||
|
||||
if self.success_msg:
|
||||
return Column(
|
||||
Text(f"{self.success_msg}", margin=1, style=TextStyle(fill=self.session.theme.success_color, font_size=1.1),
|
||||
overflow="wrap", justify="center"),
|
||||
Row(
|
||||
Button(
|
||||
Text("Zurück",
|
||||
margin=1,
|
||||
style=TextStyle(fill=self.session.theme.success_color, font_size=1.1),
|
||||
overflow="wrap",
|
||||
justify="center"
|
||||
),
|
||||
shape="rounded",
|
||||
style="plain-text",
|
||||
on_press=self.cancel_cb
|
||||
)
|
||||
),
|
||||
min_height=10
|
||||
)
|
||||
|
||||
if self.error_msg:
|
||||
return Column(
|
||||
Text(f"{self.error_msg}", margin=1, style=TextStyle(fill=self.session.theme.danger_color, font_size=1.1),
|
||||
overflow="wrap", justify="center"),
|
||||
Row(
|
||||
Button(
|
||||
Text("Zurück",
|
||||
margin=1,
|
||||
style=TextStyle(fill=self.session.theme.danger_color, font_size=1.1),
|
||||
overflow="wrap",
|
||||
justify="center"
|
||||
),
|
||||
shape="rounded",
|
||||
style="plain-text",
|
||||
on_press=self.cancel_cb
|
||||
)
|
||||
),
|
||||
min_height=10
|
||||
)
|
||||
|
||||
return Column(
|
||||
Text(f"Sitzplatz {self.seat_id} verbindlich buchen?", margin=1, style=TextStyle(fill=self.session.theme.neutral_color, font_size=1.4), overflow="wrap", justify="center"),
|
||||
Row(
|
||||
Button(
|
||||
Text("Nein",
|
||||
margin=1,
|
||||
style=TextStyle(fill=self.session.theme.danger_color, font_size=1.1),
|
||||
overflow="wrap",
|
||||
justify="center"
|
||||
),
|
||||
shape="rounded",
|
||||
style="plain-text",
|
||||
on_press=self.cancel_cb
|
||||
),
|
||||
Button(
|
||||
Text("Ja",
|
||||
margin=1,
|
||||
style=TextStyle(fill=self.session.theme.success_color, font_size=1.1),
|
||||
overflow="wrap",
|
||||
justify="center"
|
||||
),
|
||||
shape="rounded",
|
||||
style="minor",
|
||||
on_press=self.confirm_cb
|
||||
)
|
||||
),
|
||||
min_height=10
|
||||
)
|
||||
@@ -0,0 +1,217 @@
|
||||
from asyncio import sleep, create_task
|
||||
from decimal import Decimal
|
||||
|
||||
import rio
|
||||
from rio import Component, Column, Text, TextStyle, Button, Row, ScrollContainer, Spacer, Popup, Table, event
|
||||
|
||||
from src.ezgg_lan_manager.components.CateringCartItem import CateringCartItem
|
||||
from src.ezgg_lan_manager.components.CateringOrderItem import CateringOrderItem
|
||||
from src.ezgg_lan_manager.services.AccountingService import AccountingService
|
||||
from src.ezgg_lan_manager.services.CateringService import CateringService, CateringError, CateringErrorType
|
||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringMenuItemsWithAmount
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
|
||||
POPUP_CLOSE_TIMEOUT_SECONDS = 3
|
||||
|
||||
class ShoppingCartAndOrders(Component):
|
||||
show_cart: bool = True
|
||||
orders: list[CateringOrder] = []
|
||||
order_button_loading: bool = False
|
||||
popup_message: str = ""
|
||||
popup_is_shown: bool = False
|
||||
popup_is_error: bool = True
|
||||
|
||||
@event.periodic(5)
|
||||
async def periodic_refresh_of_orders(self) -> None:
|
||||
if not self.show_cart and not self.popup_is_shown:
|
||||
self.orders = await self.session[CateringService].get_orders_for_user(self.session[SessionStorage].user_id)
|
||||
|
||||
async def switch(self) -> None:
|
||||
self.show_cart = not self.show_cart
|
||||
self.orders = await self.session[CateringService].get_orders_for_user(self.session[SessionStorage].user_id)
|
||||
|
||||
async def on_remove_item(self, list_id: int) -> None:
|
||||
catering_service = self.session[CateringService]
|
||||
user_id = self.session[SessionStorage].user_id
|
||||
cart = catering_service.get_cart(user_id)
|
||||
try:
|
||||
cart.pop(list_id)
|
||||
except IndexError:
|
||||
return
|
||||
catering_service.save_cart(user_id, cart)
|
||||
self.force_refresh()
|
||||
|
||||
async def on_empty_cart_pressed(self) -> None:
|
||||
self.session[CateringService].save_cart(self.session[SessionStorage].user_id, [])
|
||||
self.force_refresh()
|
||||
|
||||
async def on_add_item(self, article_id: int) -> None:
|
||||
catering_service = self.session[CateringService]
|
||||
user_id = self.session[SessionStorage].user_id
|
||||
if not user_id:
|
||||
return
|
||||
cart = catering_service.get_cart(user_id)
|
||||
item_to_add = await catering_service.get_menu_item_by_id(article_id)
|
||||
cart.append(item_to_add)
|
||||
catering_service.save_cart(user_id, cart)
|
||||
self.force_refresh()
|
||||
|
||||
async def show_popup(self, text: str, is_error: bool) -> None:
|
||||
self.popup_is_error = is_error
|
||||
self.popup_message = text
|
||||
self.popup_is_shown = True
|
||||
self.force_refresh()
|
||||
await sleep(POPUP_CLOSE_TIMEOUT_SECONDS)
|
||||
self.popup_is_shown = False
|
||||
self.force_refresh()
|
||||
|
||||
async def on_order_pressed(self) -> None:
|
||||
self.order_button_loading = True
|
||||
self.force_refresh()
|
||||
|
||||
user_id = self.session[SessionStorage].user_id
|
||||
cart = self.session[CateringService].get_cart(user_id)
|
||||
show_popup_task = None
|
||||
if len(cart) < 1:
|
||||
show_popup_task = create_task(self.show_popup("Warenkorb leer", True))
|
||||
else:
|
||||
items_with_amounts: CateringMenuItemsWithAmount = {}
|
||||
for item in cart:
|
||||
try:
|
||||
items_with_amounts[item] += 1
|
||||
except KeyError:
|
||||
items_with_amounts[item] = 1
|
||||
try:
|
||||
await self.session[CateringService].place_order(items_with_amounts, user_id)
|
||||
except CateringError as catering_error:
|
||||
if catering_error.error_type == CateringErrorType.INCLUDES_DISABLED_ITEM:
|
||||
show_popup_task = create_task(self.show_popup("Warenkorb enthält gesperrte Artikel", True))
|
||||
elif catering_error.error_type == CateringErrorType.INSUFFICIENT_FUNDS:
|
||||
show_popup_task = create_task(self.show_popup("Guthaben nicht ausreichend", True))
|
||||
else:
|
||||
show_popup_task = create_task(self.show_popup("Unbekannter Fehler", True))
|
||||
else:
|
||||
self.session[CateringService].save_cart(self.session[SessionStorage].user_id, [])
|
||||
self.order_button_loading = False
|
||||
if not show_popup_task:
|
||||
show_popup_task = create_task(self.show_popup("Bestellung erfolgreich aufgegeben!", False))
|
||||
|
||||
async def _create_order_info_modal(self, order: CateringOrder) -> None:
|
||||
def build_dialog_content() -> rio.Component:
|
||||
# @todo: rio 0.10.8 did not have the ability to align the columns, check back in a future version
|
||||
table = Table(
|
||||
{
|
||||
"Artikel": [item.name for item in order.items.keys()] + ["Gesamtpreis:"],
|
||||
"Anzahl": [item for item in order.items.values()] + [""],
|
||||
"Preis": [AccountingService.make_euro_string_from_decimal(item.price) for item in order.items.keys()] + [AccountingService.make_euro_string_from_decimal(order.price)],
|
||||
},
|
||||
show_row_numbers=False
|
||||
)
|
||||
return rio.Card(
|
||||
rio.Column(
|
||||
rio.Text(
|
||||
f"Deine Bestellung ({order.order_id})",
|
||||
align_x=0.5,
|
||||
margin_bottom=0.5
|
||||
),
|
||||
table,
|
||||
margin=2,
|
||||
),
|
||||
align_x=0.5,
|
||||
align_y=0.2,
|
||||
min_width=50,
|
||||
min_height=10,
|
||||
color=self.session.theme.primary_color,
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
margin_top=2,
|
||||
margin_bottom=1,
|
||||
)
|
||||
dialog = await self.session.show_custom_dialog(
|
||||
build=build_dialog_content,
|
||||
modal=True,
|
||||
user_closable=True,
|
||||
)
|
||||
await dialog.wait_for_close()
|
||||
|
||||
def build(self) -> rio.Component:
|
||||
user_id = self.session[SessionStorage].user_id
|
||||
catering_service = self.session[CateringService]
|
||||
cart = catering_service.get_cart(user_id)
|
||||
if self.show_cart:
|
||||
cart_container = ScrollContainer(
|
||||
content=Column(
|
||||
*[CateringCartItem(
|
||||
article_name=cart_item.name,
|
||||
article_price=cart_item.price,
|
||||
article_id=cart_item.item_id,
|
||||
remove_item_cb=self.on_remove_item,
|
||||
list_id=idx
|
||||
) for idx, cart_item in enumerate(cart)],
|
||||
Spacer(grow_y=True)
|
||||
),
|
||||
min_height=8,
|
||||
min_width=33,
|
||||
margin=1
|
||||
)
|
||||
return Column(
|
||||
cart_container,
|
||||
Popup(
|
||||
anchor=cart_container,
|
||||
content=Text(self.popup_message, style=TextStyle(fill=self.session.theme.danger_color if self.popup_is_error else self.session.theme.success_color), overflow="wrap", margin=2, justify="center", min_width=20),
|
||||
is_open=self.popup_is_shown,
|
||||
position="center",
|
||||
color=self.session.theme.primary_color
|
||||
),
|
||||
Row(
|
||||
Text(
|
||||
text=f"Preis: {AccountingService.make_euro_string_from_decimal(sum((cart_item.price for cart_item in cart), Decimal(0)))}",
|
||||
style=TextStyle(
|
||||
fill=self.session.theme.background_color,
|
||||
font_size=0.8
|
||||
),
|
||||
margin=1
|
||||
),
|
||||
Button(
|
||||
content=Text(
|
||||
"Warenkorb leeren",
|
||||
style=TextStyle(fill=self.session.theme.danger_color, font_size=0.9),
|
||||
align_x=0.2
|
||||
),
|
||||
margin=1,
|
||||
margin_left=0,
|
||||
shape="rectangle",
|
||||
style="major",
|
||||
color="primary",
|
||||
on_press=self.on_empty_cart_pressed
|
||||
),
|
||||
Button(
|
||||
content=Text(
|
||||
"Bestellen",
|
||||
style=TextStyle(fill=self.session.theme.success_color, font_size=0.9),
|
||||
align_x=0.2
|
||||
),
|
||||
margin=1,
|
||||
margin_left=0,
|
||||
shape="rectangle",
|
||||
style="major",
|
||||
color="primary",
|
||||
on_press=self.on_order_pressed,
|
||||
is_loading=self.order_button_loading
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
orders_container = ScrollContainer(
|
||||
content=Column(
|
||||
*[CateringOrderItem(
|
||||
order=order_item,
|
||||
info_modal_cb=self._create_order_info_modal
|
||||
) for order_item in self.orders],
|
||||
Spacer(grow_y=True)
|
||||
),
|
||||
min_height=8,
|
||||
min_width=33,
|
||||
margin=1
|
||||
)
|
||||
return Column(orders_container)
|
||||
@@ -0,0 +1,89 @@
|
||||
from functools import partial
|
||||
from typing import Callable, Optional
|
||||
from decimal import Decimal
|
||||
|
||||
import rio
|
||||
from rio import Component, Card, Column, Text, Row, Button, TextStyle, ProgressBar, event, Spacer
|
||||
|
||||
from src.ezgg_lan_manager import TicketingService
|
||||
from src.ezgg_lan_manager.services.AccountingService import AccountingService
|
||||
from src.ezgg_lan_manager.types.Ticket import Ticket
|
||||
|
||||
|
||||
class TicketBuyCard(Component):
|
||||
description: str
|
||||
additional_info: str
|
||||
price: Decimal
|
||||
category: str
|
||||
pressed_cb: Callable
|
||||
is_enabled: bool
|
||||
total_tickets: int
|
||||
user_ticket: Optional[Ticket]
|
||||
available_tickets: int = 0
|
||||
|
||||
@event.on_populate
|
||||
async def async_init(self) -> None:
|
||||
self.available_tickets = await self.session[TicketingService].get_available_tickets_for_category(self.category)
|
||||
|
||||
def build(self) -> rio.Component:
|
||||
ticket_description_style = TextStyle(
|
||||
fill=self.session.theme.neutral_color,
|
||||
font_size=1.2,
|
||||
)
|
||||
ticket_additional_info_style = TextStyle(
|
||||
fill=self.session.theme.neutral_color,
|
||||
font_size=0.8
|
||||
)
|
||||
ticket_owned_style = TextStyle(
|
||||
fill=self.session.theme.success_color,
|
||||
font_size=0.8
|
||||
)
|
||||
|
||||
try:
|
||||
progress = self.available_tickets / self.total_tickets
|
||||
except ZeroDivisionError:
|
||||
progress = 0
|
||||
progress_bar = ProgressBar(
|
||||
progress=progress,
|
||||
color=self.session.theme.success_color if progress > 0.25 else self.session.theme.danger_color,
|
||||
margin_right=1,
|
||||
grow_x=True
|
||||
)
|
||||
|
||||
tickets_side_text = Text(
|
||||
f"{self.available_tickets}/{self.total_tickets}",
|
||||
align_x=1
|
||||
)
|
||||
|
||||
return Card(
|
||||
Column(
|
||||
Text(self.description, margin_left=1, margin_top=1, style=ticket_description_style),
|
||||
Text("Du besitzt dieses Ticket!", margin_left=1, margin_top=1, style=ticket_owned_style) if self.user_ticket is not None and self.user_ticket.category == self.category else Spacer(),
|
||||
Text(self.additional_info, margin_left=1, margin_top=1, style=ticket_additional_info_style, overflow="wrap"),
|
||||
Row(
|
||||
progress_bar,
|
||||
tickets_side_text,
|
||||
margin_top=1,
|
||||
margin_left=1,
|
||||
margin_right=1
|
||||
),
|
||||
Row(
|
||||
Text(f"{AccountingService.make_euro_string_from_decimal(self.price)}", margin_left=1, margin_top=1, grow_x=True),
|
||||
Button(
|
||||
Text("Kaufen", align_x=0.5, margin=0.4),
|
||||
margin_right=1,
|
||||
margin_top=1,
|
||||
style="major",
|
||||
shape="rounded",
|
||||
on_press=partial(self.pressed_cb, self.category),
|
||||
is_sensitive=self.is_enabled
|
||||
),
|
||||
margin_bottom=1
|
||||
)
|
||||
),
|
||||
margin_left=3,
|
||||
margin_right=3,
|
||||
margin_bottom=1,
|
||||
color=self.session.theme.hud_color,
|
||||
corner_radius=0.2
|
||||
)
|
||||
@@ -0,0 +1,250 @@
|
||||
from datetime import date
|
||||
from hashlib import sha256
|
||||
from typing import Optional
|
||||
|
||||
from email_validator import validate_email, EmailNotValidError
|
||||
from from_root import from_root
|
||||
from rio import Component, Column, Button, Color, TextStyle, Text, TextInput, Row, Image, event, Spacer, DateInput, \
|
||||
TextInputChangeEvent, NoFileSelectedError
|
||||
|
||||
from src.ezgg_lan_manager.services.UserService import UserService, NameNotAllowedError
|
||||
from src.ezgg_lan_manager.services.ConfigurationService import ConfigurationService
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
|
||||
class UserEditForm(Component):
|
||||
is_own_profile: bool = True
|
||||
profile_picture: Optional[bytes] = None
|
||||
user: Optional[User] = None
|
||||
|
||||
input_user_name: str = ""
|
||||
input_user_mail: str = ""
|
||||
input_user_first_name: str = ""
|
||||
input_user_last_name: str = ""
|
||||
input_password_1: str = ""
|
||||
input_password_2: str = ""
|
||||
input_birthday: date = date.today()
|
||||
|
||||
is_email_valid: bool = True
|
||||
|
||||
result_text: str = ""
|
||||
result_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} - Profil bearbeiten")
|
||||
if self.is_own_profile:
|
||||
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||
self.profile_picture = await self.session[UserService].get_profile_picture(self.user.user_id)
|
||||
else:
|
||||
self.profile_picture = await self.session[UserService].get_profile_picture(self.user.user_id)
|
||||
|
||||
self.input_user_name = self.user.user_name
|
||||
self.input_user_mail = self.user.user_mail
|
||||
self.input_user_first_name = self.optional_str_to_str(self.user.user_first_name)
|
||||
self.input_user_last_name = self.optional_str_to_str(self.user.user_last_name)
|
||||
self.input_birthday = self.user.user_birth_day if self.user.user_birth_day else date.today()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def optional_str_to_str(s: Optional[str]) -> str:
|
||||
if s:
|
||||
return s
|
||||
return ""
|
||||
|
||||
def on_email_changed(self, change_event: TextInputChangeEvent) -> None:
|
||||
try:
|
||||
validate_email(change_event.text, check_deliverability=False)
|
||||
self.is_email_valid = True
|
||||
except EmailNotValidError:
|
||||
self.is_email_valid = False
|
||||
|
||||
async def upload_new_pfp(self) -> None:
|
||||
try:
|
||||
new_pfp = await self.session.pick_file(file_types=("png", "jpg", "jpeg"), multiple=False)
|
||||
except NoFileSelectedError:
|
||||
self.result_text = "Keine Datei ausgewählt!"
|
||||
self.result_success = False
|
||||
return
|
||||
|
||||
if new_pfp.size_in_bytes > 2 * 1_000_000:
|
||||
self.result_text = "Bild zu groß! (> 2MB)"
|
||||
self.result_success = False
|
||||
return
|
||||
|
||||
image_data = await new_pfp.read_bytes()
|
||||
await self.session[UserService].set_profile_picture(self.user.user_id, image_data)
|
||||
self.profile_picture = image_data
|
||||
self.result_text = "Gespeichert!"
|
||||
self.result_success = True
|
||||
|
||||
async def remove_profile_picture(self) -> None:
|
||||
await self.session[UserService].remove_profile_picture(self.user.user_id)
|
||||
self.profile_picture = None
|
||||
self.result_text = "Profilbild entfernt!"
|
||||
self.result_success = True
|
||||
|
||||
async def on_save_pressed(self) -> None:
|
||||
if not all((self.is_email_valid, self.input_user_name, self.input_user_mail)):
|
||||
self.result_text = "Ungültige Werte!"
|
||||
self.result_success = False
|
||||
return
|
||||
|
||||
if len(self.input_password_1.strip()) > 0:
|
||||
if self.input_password_1.strip() != self.input_password_2.strip():
|
||||
self.result_text = "Passwörter nicht gleich!"
|
||||
self.result_success = False
|
||||
return
|
||||
|
||||
self.user.user_mail = self.input_user_mail
|
||||
|
||||
if self.input_birthday == date.today():
|
||||
self.user.user_birth_day = None
|
||||
else:
|
||||
self.user.user_birth_day = self.input_birthday
|
||||
|
||||
self.user.user_first_name = self.input_user_first_name
|
||||
self.user.user_last_name = self.input_user_last_name
|
||||
self.user.user_name = self.input_user_name
|
||||
if len(self.input_password_1.strip()) > 0:
|
||||
self.user.user_password = sha256(self.input_password_1.strip().encode(encoding="utf-8")).hexdigest()
|
||||
|
||||
try:
|
||||
await self.session[UserService].update_user(self.user)
|
||||
except NameNotAllowedError:
|
||||
self.result_text = "Ungültige Zeichen in Nutzername"
|
||||
self.result_success = False
|
||||
return
|
||||
|
||||
self.result_text = "Gespeichert!"
|
||||
self.result_success = True
|
||||
|
||||
def build(self) -> Component:
|
||||
pfp_image_container = Image(
|
||||
from_root("src/ezgg_lan_manager/assets/img/anon_pfp.png") if self.profile_picture is None else self.profile_picture,
|
||||
align_x=0.5,
|
||||
min_width=10,
|
||||
min_height=10,
|
||||
margin_top=1,
|
||||
margin_bottom=1
|
||||
)
|
||||
|
||||
return Column(
|
||||
pfp_image_container,
|
||||
Button(
|
||||
content=Text(
|
||||
"Neues Bild hochladen",
|
||||
style=TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
|
||||
),
|
||||
align_x=0.5,
|
||||
margin_bottom=1,
|
||||
shape="rectangle",
|
||||
style="major",
|
||||
color="primary",
|
||||
on_press=self.upload_new_pfp
|
||||
) if self.is_own_profile else Button(
|
||||
content=Text(
|
||||
"Bild löschen",
|
||||
style=TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
|
||||
),
|
||||
align_x=0.5,
|
||||
margin_bottom=1,
|
||||
shape="rectangle",
|
||||
style="major",
|
||||
color="primary",
|
||||
on_press=self.remove_profile_picture
|
||||
),
|
||||
Row(
|
||||
TextInput(
|
||||
label=f"{'Deine ' if self.is_own_profile else ''}User-ID",
|
||||
text=str(self.user.user_id),
|
||||
is_sensitive=False,
|
||||
margin_left=1,
|
||||
grow_x=False
|
||||
),
|
||||
TextInput(
|
||||
label=f"{'Dein ' if self.is_own_profile else ''}Nickname",
|
||||
text=self.bind().input_user_name,
|
||||
is_sensitive=not self.is_own_profile,
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
grow_x=True
|
||||
),
|
||||
margin_bottom=1
|
||||
),
|
||||
TextInput(
|
||||
label="E-Mail Adresse",
|
||||
text=self.bind().input_user_mail,
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
margin_bottom=1,
|
||||
grow_x=True,
|
||||
is_valid=self.is_email_valid,
|
||||
on_change=self.on_email_changed
|
||||
),
|
||||
Row(
|
||||
TextInput(
|
||||
label="Vorname",
|
||||
text=self.bind().input_user_first_name,
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
grow_x=True
|
||||
),
|
||||
TextInput(
|
||||
label="Nachname",
|
||||
text=self.bind().input_user_last_name,
|
||||
margin_right=1,
|
||||
grow_x=True
|
||||
),
|
||||
margin_bottom=1
|
||||
),
|
||||
DateInput(
|
||||
value=self.bind().input_birthday,
|
||||
label="Geburtstag",
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
margin_bottom=1,
|
||||
grow_x=True
|
||||
),
|
||||
TextInput(
|
||||
label="Neues Passwort setzen",
|
||||
text=self.bind().input_password_1,
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
margin_bottom=1,
|
||||
grow_x=True,
|
||||
is_secret=True
|
||||
),
|
||||
TextInput(
|
||||
label="Neues Passwort wiederholen",
|
||||
text=self.bind().input_password_2,
|
||||
margin_left=1,
|
||||
margin_right=1,
|
||||
margin_bottom=1,
|
||||
grow_x=True,
|
||||
is_secret=True
|
||||
),
|
||||
|
||||
Row(
|
||||
Text(
|
||||
text=self.bind().result_text,
|
||||
style=TextStyle(fill=self.session.theme.success_color if self.result_success else self.session.theme.danger_color),
|
||||
margin_left=1
|
||||
),
|
||||
Button(
|
||||
content=Text(
|
||||
"Speichern",
|
||||
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_save_pressed
|
||||
),
|
||||
)
|
||||
) if self.user else Spacer()
|
||||
@@ -0,0 +1,15 @@
|
||||
import logging
|
||||
|
||||
from rio import Component
|
||||
from src.ezgg_lan_manager.components.LoginBox import LoginBox
|
||||
from src.ezgg_lan_manager.components.UserInfoBox import UserInfoBox
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
class UserInfoAndLoginBox(Component):
|
||||
def build(self) -> Component:
|
||||
if self.session[SessionStorage].user_id is None:
|
||||
return LoginBox(status_change_cb=self.force_refresh)
|
||||
else:
|
||||
return UserInfoBox(status_change_cb=self.force_refresh)
|
||||
@@ -0,0 +1,121 @@
|
||||
from random import choice
|
||||
from typing import Optional
|
||||
from decimal import Decimal
|
||||
|
||||
from rio import Component, TextStyle, Color, Button, Text, Rectangle, Column, Row, Spacer, Link, event, EventHandler
|
||||
|
||||
from src.ezgg_lan_manager.components.UserInfoBoxButton import UserInfoBoxButton
|
||||
from src.ezgg_lan_manager.services.LocalDataService import LocalData, LocalDataService
|
||||
from src.ezgg_lan_manager.services.UserService import UserService
|
||||
from src.ezgg_lan_manager.services.AccountingService import AccountingService
|
||||
from src.ezgg_lan_manager.services.TicketingService import TicketingService
|
||||
from src.ezgg_lan_manager.services.SeatingService import SeatingService
|
||||
from src.ezgg_lan_manager.types.Seat import Seat
|
||||
from src.ezgg_lan_manager.types.Ticket import Ticket
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
|
||||
|
||||
class StatusButton(Component):
|
||||
STYLE = TextStyle(fill=Color.from_hex("121212"), font_size=0.5)
|
||||
label: str
|
||||
target_url: str
|
||||
enabled: bool
|
||||
|
||||
def build(self) -> Component:
|
||||
return Link(
|
||||
content=Button(
|
||||
content=Text(self.label, style=self.STYLE, justify="center"),
|
||||
shape="rectangle",
|
||||
style="major",
|
||||
color="success" if self.enabled else "danger",
|
||||
grow_x=True,
|
||||
margin_left=0.6,
|
||||
margin_right=0.6,
|
||||
margin_top=0.6
|
||||
),
|
||||
target_url=self.target_url,
|
||||
align_y=0.5,
|
||||
grow_y=False
|
||||
)
|
||||
|
||||
|
||||
class UserInfoBox(Component):
|
||||
status_change_cb: EventHandler = None
|
||||
TEXT_STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9)
|
||||
user: Optional[User] = None
|
||||
user_balance: Optional[Decimal] = Decimal("0")
|
||||
user_ticket: Optional[Ticket] = None
|
||||
user_seat: Optional[Seat] = None
|
||||
|
||||
@staticmethod
|
||||
def get_greeting() -> str:
|
||||
return choice(["Guten Tacho", "Tuten Gag", "Servus", "Moinjour", "Hallöchen", "Heyho", "Moinsen"])
|
||||
|
||||
async def logout(self) -> None:
|
||||
await self.session[SessionStorage].clear()
|
||||
self.user = None
|
||||
self.session[LocalDataService].del_session(self.session[LocalData].stored_session_token)
|
||||
self.session[LocalData].stored_session_token = None
|
||||
self.session.attach(self.session[LocalData])
|
||||
self.status_change_cb()
|
||||
self.session.navigate_to("/")
|
||||
|
||||
@event.on_populate
|
||||
async def async_init(self) -> None:
|
||||
if self.session[SessionStorage].user_id:
|
||||
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||
self.user_balance = await self.session[AccountingService].get_balance(self.user.user_id)
|
||||
self.user_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id)
|
||||
self.user_seat = await self.session[SeatingService].get_user_seat(self.user.user_id)
|
||||
self.session[AccountingService].add_update_hook(self.update)
|
||||
|
||||
async def update(self) -> None:
|
||||
if not self.user:
|
||||
self.user = await self.session[UserService].get_user(self.session[SessionStorage].user_id)
|
||||
if not self.user:
|
||||
return
|
||||
self.user_balance = await self.session[AccountingService].get_balance(self.user.user_id)
|
||||
self.user_ticket = await self.session[TicketingService].get_user_ticket(self.user.user_id)
|
||||
self.user_seat = await self.session[SeatingService].get_user_seat(self.user.user_id)
|
||||
|
||||
def build(self) -> Component:
|
||||
if not self.user:
|
||||
return Spacer()
|
||||
return Rectangle(
|
||||
content=Column(
|
||||
Text(f"{self.get_greeting()},", style=TextStyle(fill=Color.from_hex("02dac5"), font_size=0.9),
|
||||
justify="center"),
|
||||
Text(f"{self.user.user_name}", style=TextStyle(fill=Color.from_hex("02dac5"), font_size=1.2),
|
||||
justify="center"),
|
||||
Row(
|
||||
StatusButton(label="TICKET", target_url="./buy_ticket",
|
||||
enabled=self.user_ticket is not None),
|
||||
StatusButton(label="SITZPLATZ", target_url="./seating",
|
||||
enabled=self.user_seat is not None),
|
||||
proportions=(50, 50),
|
||||
grow_y=False
|
||||
),
|
||||
UserInfoBoxButton("Profil bearbeiten", "./edit-profile"),
|
||||
UserInfoBoxButton(
|
||||
f"Guthaben: {self.session[AccountingService].make_euro_string_from_decimal(self.user_balance)}",
|
||||
"./account"),
|
||||
Button(
|
||||
content=Text("Ausloggen", style=TextStyle(fill=Color.from_hex("02dac5"), font_size=0.6)),
|
||||
shape="rectangle",
|
||||
style="minor",
|
||||
color="secondary",
|
||||
grow_x=True,
|
||||
margin_left=0.6,
|
||||
margin_right=0.6,
|
||||
margin_top=0.6,
|
||||
on_press=self.logout
|
||||
)
|
||||
),
|
||||
fill=Color.TRANSPARENT,
|
||||
min_height=8,
|
||||
min_width=12,
|
||||
align_x=0.5,
|
||||
margin_top=0.3,
|
||||
margin_bottom=2
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
from rio import Component, TextStyle, Color, Link, Button, Text
|
||||
|
||||
|
||||
class UserInfoBoxButton(Component):
|
||||
STYLE = TextStyle(fill=Color.from_hex("02dac5"), font_size=0.6)
|
||||
label: str
|
||||
target_url: str
|
||||
open_new_tab: bool = False
|
||||
|
||||
def build(self) -> Component:
|
||||
return Link(
|
||||
content=Button(
|
||||
content=Text(self.label, style=self.STYLE),
|
||||
shape="rectangle",
|
||||
style="minor",
|
||||
color="secondary",
|
||||
grow_x=True,
|
||||
margin_left=0.6,
|
||||
margin_right=0.6,
|
||||
margin_top=0.6
|
||||
),
|
||||
target_url=self.target_url,
|
||||
open_in_new_tab=self.open_new_tab
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
from typing import Optional
|
||||
|
||||
from rio import URL, GuardEvent
|
||||
|
||||
from src.ezgg_lan_manager.services.UserService import UserService
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
|
||||
|
||||
# Guards pages against access from users that are NOT logged in
|
||||
def logged_in_guard(event: GuardEvent) -> Optional[URL]:
|
||||
if event.session[SessionStorage].user_id is None:
|
||||
return URL("./")
|
||||
|
||||
# Guards pages against access from users that ARE logged in
|
||||
def not_logged_in_guard(event: GuardEvent) -> Optional[URL]:
|
||||
if event.session[SessionStorage].user_id is not None:
|
||||
return URL("./")
|
||||
|
||||
# Guards pages against access from users that are NOT logged in and NOT team members
|
||||
def team_guard(event: GuardEvent) -> Optional[URL]:
|
||||
user_id = event.session[SessionStorage].user_id
|
||||
is_team_member = event.session[SessionStorage].is_team_member
|
||||
if user_id is None or not is_team_member:
|
||||
return URL("./")
|
||||
@@ -0,0 +1,200 @@
|
||||
# USE THIS ON AN EMPTY DATABASE TO GENERATE DEMO DATA
|
||||
import asyncio
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
import sys
|
||||
|
||||
from src.ezgg_lan_manager import init_services
|
||||
from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItemCategory
|
||||
from src.ezgg_lan_manager.types.News import News
|
||||
|
||||
DEMO_USERS = [
|
||||
{"user_name": "manfred", "user_mail": "manfred@demomail.com", "password_clear_text": "manfred"}, # Gast
|
||||
{"user_name": "gustav", "user_mail": "gustav@demomail.com", "password_clear_text": "gustav"}, # Gast + Ticket(NORMAL)
|
||||
{"user_name": "jason", "user_mail": "juergen@demomail.com", "password_clear_text": "jason"}, # Gast + Ticket(NORMAL) + Sitzplatz
|
||||
{"user_name": "lisa", "user_mail": "lisa@demomail.com", "password_clear_text": "lisa"}, # Teamler
|
||||
{"user_name": "thomas", "user_mail": "thomas@demomail.com", "password_clear_text": "thomas"} # Teamler + Admin
|
||||
]
|
||||
|
||||
|
||||
async def run() -> None:
|
||||
services = init_services()
|
||||
await services[3].init_db_pool()
|
||||
catering_service = services[1]
|
||||
user_service = services[8]
|
||||
accounting_service = services[0]
|
||||
ticket_service = services[7]
|
||||
seating_service = services[6]
|
||||
news_service = services[5]
|
||||
|
||||
if input("Generate seating table? (y/N): ").lower() == "y":
|
||||
sys.exit("This part of the script is currently being reworked... :(")
|
||||
|
||||
if not input("Generate users? (Y/n): ").lower() == "n":
|
||||
# MANFRED
|
||||
manfred = await user_service.create_user(DEMO_USERS[0]["user_name"], DEMO_USERS[0]["user_mail"],
|
||||
DEMO_USERS[0]["password_clear_text"])
|
||||
|
||||
# GUSTAV
|
||||
gustav = await user_service.create_user(DEMO_USERS[1]["user_name"], DEMO_USERS[1]["user_mail"],
|
||||
DEMO_USERS[1]["password_clear_text"])
|
||||
await accounting_service.add_balance(gustav.user_id, Decimal("1000.00"), "DEMO EINZAHLUNG")
|
||||
await ticket_service.purchase_ticket(gustav.user_id, "NORMAL")
|
||||
|
||||
# JASON
|
||||
jason = await user_service.create_user(DEMO_USERS[2]["user_name"], DEMO_USERS[2]["user_mail"],
|
||||
DEMO_USERS[2]["password_clear_text"])
|
||||
await accounting_service.add_balance(jason.user_id, Decimal("1000.00"), "DEMO EINZAHLUNG")
|
||||
await ticket_service.purchase_ticket(jason.user_id, "NORMAL")
|
||||
|
||||
# LISA
|
||||
lisa = await user_service.create_user(DEMO_USERS[3]["user_name"], DEMO_USERS[3]["user_mail"],
|
||||
DEMO_USERS[3]["password_clear_text"])
|
||||
await accounting_service.add_balance(lisa.user_id, Decimal("1000.00"), "DEMO EINZAHLUNG")
|
||||
lisa.is_team_member = True
|
||||
await user_service.update_user(lisa)
|
||||
|
||||
# THOMAS
|
||||
thomas = await user_service.create_user(DEMO_USERS[4]["user_name"], DEMO_USERS[4]["user_mail"],
|
||||
DEMO_USERS[4]["password_clear_text"])
|
||||
await accounting_service.add_balance(thomas.user_id, Decimal("1000.00"), "DEMO EINZAHLUNG")
|
||||
thomas.is_team_member = True
|
||||
thomas.is_admin = True
|
||||
await user_service.update_user(thomas)
|
||||
|
||||
if not input("Generate catering menu? (Y/n): ").lower() == "n":
|
||||
# MAIN_COURSE
|
||||
await catering_service.add_menu_item("Schnitzel Wiener Art", "mit Pommes", Decimal("10.00"),
|
||||
CateringMenuItemCategory.MAIN_COURSE)
|
||||
await catering_service.add_menu_item("Jäger Schnitzel mit Champignonrahm Sauce", "mit Pommes", Decimal("11.50"),
|
||||
CateringMenuItemCategory.MAIN_COURSE)
|
||||
await catering_service.add_menu_item("Tortellini in Käsesauce mit Fleischfüllung", "", Decimal("10.50"),
|
||||
CateringMenuItemCategory.MAIN_COURSE)
|
||||
await catering_service.add_menu_item("Tortellini in Käsesauce ohne Fleischfüllung", "Vegetarisch", Decimal("10.50"),
|
||||
CateringMenuItemCategory.MAIN_COURSE)
|
||||
|
||||
# SNACK
|
||||
await catering_service.add_menu_item("Käse Schinken Wrap", "", Decimal("5.00"), CateringMenuItemCategory.SNACK)
|
||||
await catering_service.add_menu_item("Puten Paprika Wrap", "", Decimal("7.00"), CateringMenuItemCategory.SNACK)
|
||||
await catering_service.add_menu_item("Tomate Mozzarella Wrap", "", Decimal("6.00"), CateringMenuItemCategory.SNACK)
|
||||
await catering_service.add_menu_item("Portion Pommes", "", Decimal("4.00"), CateringMenuItemCategory.SNACK)
|
||||
await catering_service.add_menu_item("Rinds-Currywurst", "", Decimal("4.50"), CateringMenuItemCategory.SNACK)
|
||||
await catering_service.add_menu_item("Rinds-Currywurst mit Pommes", "", Decimal("6.50"), CateringMenuItemCategory.SNACK)
|
||||
await catering_service.add_menu_item("Nudelsalat", "", Decimal("4.50"), CateringMenuItemCategory.SNACK)
|
||||
await catering_service.add_menu_item("Nudelsalat mit Bockwurst", "", Decimal("6.00"), CateringMenuItemCategory.SNACK)
|
||||
await catering_service.add_menu_item("Kartoffelsalat", "", Decimal("4.50"), CateringMenuItemCategory.SNACK)
|
||||
await catering_service.add_menu_item("Kartoffelsalat mit Bockwurst", "", Decimal("6.00"), CateringMenuItemCategory.SNACK)
|
||||
await catering_service.add_menu_item("Sandwichtoast - Schinken", "", Decimal("1.80"), CateringMenuItemCategory.SNACK)
|
||||
await catering_service.add_menu_item("Sandwichtoast - Käse", "", Decimal("1.80"), CateringMenuItemCategory.SNACK)
|
||||
await catering_service.add_menu_item("Sandwichtoast - Schinken/Käse", "", Decimal("2.10"), CateringMenuItemCategory.SNACK)
|
||||
await catering_service.add_menu_item("Sandwichtoast - Salami", "", Decimal("1.80"), CateringMenuItemCategory.SNACK)
|
||||
await catering_service.add_menu_item("Sandwichtoast - Salami/Käse", "", Decimal("2.10"), CateringMenuItemCategory.SNACK)
|
||||
await catering_service.add_menu_item("Chips - Western Style", "", Decimal("1.30"), CateringMenuItemCategory.SNACK)
|
||||
await catering_service.add_menu_item("Nachos - Salted", "", Decimal("1.30"), CateringMenuItemCategory.SNACK)
|
||||
|
||||
# DESSERT
|
||||
await catering_service.add_menu_item("Panna Cotta mit Erdbeersauce", "", Decimal("7.00"), CateringMenuItemCategory.DESSERT)
|
||||
await catering_service.add_menu_item("Panna Cotta mit Blaubeersauce", "", Decimal("7.00"), CateringMenuItemCategory.DESSERT)
|
||||
await catering_service.add_menu_item("Mousse au Chocolat", "", Decimal("7.00"), CateringMenuItemCategory.DESSERT)
|
||||
|
||||
# BREAKFAST
|
||||
await catering_service.add_menu_item("Fruit Loops", "", Decimal("1.50"), CateringMenuItemCategory.BREAKFAST)
|
||||
await catering_service.add_menu_item("Smacks", "", Decimal("1.50"), CateringMenuItemCategory.BREAKFAST)
|
||||
await catering_service.add_menu_item("Knuspermüsli", "Schoko", Decimal("2.00"), CateringMenuItemCategory.BREAKFAST)
|
||||
await catering_service.add_menu_item("Cini Minis", "", Decimal("2.50"), CateringMenuItemCategory.BREAKFAST)
|
||||
await catering_service.add_menu_item("Brötchen - Schinken", "mit Margarine", Decimal("1.20"),
|
||||
CateringMenuItemCategory.BREAKFAST)
|
||||
await catering_service.add_menu_item("Brötchen - Käse", "mit Margarine", Decimal("1.20"),
|
||||
CateringMenuItemCategory.BREAKFAST)
|
||||
await catering_service.add_menu_item("Brötchen - Schinken/Käse", "mit Margarine", Decimal("1.40"),
|
||||
CateringMenuItemCategory.BREAKFAST)
|
||||
await catering_service.add_menu_item("Brötchen - Salami", "mit Margarine", Decimal("1.20"),
|
||||
CateringMenuItemCategory.BREAKFAST)
|
||||
await catering_service.add_menu_item("Brötchen - Salami/Käse", "mit Margarine", Decimal("1.40"),
|
||||
CateringMenuItemCategory.BREAKFAST)
|
||||
await catering_service.add_menu_item("Brötchen - Nutella", "mit Margarine", Decimal("1.20"),
|
||||
CateringMenuItemCategory.BREAKFAST)
|
||||
|
||||
# BEVERAGE_NON_ALCOHOLIC
|
||||
await catering_service.add_menu_item("Wasser - Still", "1L Flasche", Decimal("2.00"),
|
||||
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
|
||||
await catering_service.add_menu_item("Wasser - Medium", "1L Flasche", Decimal("2.00"),
|
||||
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
|
||||
await catering_service.add_menu_item("Wasser - Spritzig", "1L Flasche", Decimal("2.00"),
|
||||
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
|
||||
await catering_service.add_menu_item("Coca-Cola", "1L Flasche", Decimal("2.00"),
|
||||
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
|
||||
await catering_service.add_menu_item("Coca-Cola Zero", "1L Flasche", Decimal("2.00"),
|
||||
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
|
||||
await catering_service.add_menu_item("Fanta", "1L Flasche", Decimal("2.00"),
|
||||
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
|
||||
await catering_service.add_menu_item("Sprite", "1L Flasche", Decimal("2.00"),
|
||||
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
|
||||
await catering_service.add_menu_item("Spezi", "von Paulaner, 0,5L Flasche", Decimal("1.50"),
|
||||
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
|
||||
await catering_service.add_menu_item("Red Bull", "", Decimal("2.00"), CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
|
||||
await catering_service.add_menu_item("Energy", "Hausmarke", Decimal("1.50"),
|
||||
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
|
||||
|
||||
# BEVERAGE_ALCOHOLIC
|
||||
await catering_service.add_menu_item("Pils", "0,33L Flasche", Decimal("1.90"), CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
|
||||
await catering_service.add_menu_item("Radler", "0,33L Flasche", Decimal("1.90"),
|
||||
CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
|
||||
await catering_service.add_menu_item("Diesel", "0,33L Flasche", Decimal("1.90"),
|
||||
CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
|
||||
await catering_service.add_menu_item("Apfelwein Pur", "0,33L Flasche", Decimal("1.90"),
|
||||
CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
|
||||
await catering_service.add_menu_item("Apfelwein Sauer", "0,33L Flasche", Decimal("1.90"),
|
||||
CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
|
||||
await catering_service.add_menu_item("Apfelwein Cola", "0,33L Flasche", Decimal("1.90"),
|
||||
CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
|
||||
|
||||
# BEVERAGE_COCKTAIL
|
||||
await catering_service.add_menu_item("Vodka Energy", "", Decimal("4.00"), CateringMenuItemCategory.BEVERAGE_COCKTAIL)
|
||||
await catering_service.add_menu_item("Vodka O-Saft", "", Decimal("4.00"), CateringMenuItemCategory.BEVERAGE_COCKTAIL)
|
||||
await catering_service.add_menu_item("Whiskey Cola", "mit Bourbon", Decimal("4.00"),
|
||||
CateringMenuItemCategory.BEVERAGE_COCKTAIL)
|
||||
await catering_service.add_menu_item("Jägermeister Energy", "", Decimal("4.00"), CateringMenuItemCategory.BEVERAGE_COCKTAIL)
|
||||
await catering_service.add_menu_item("Sex on the Beach", "", Decimal("5.50"), CateringMenuItemCategory.BEVERAGE_COCKTAIL)
|
||||
await catering_service.add_menu_item("Long Island Ice Tea", "", Decimal("5.50"), CateringMenuItemCategory.BEVERAGE_COCKTAIL)
|
||||
await catering_service.add_menu_item("Caipirinha", "", Decimal("5.50"), CateringMenuItemCategory.BEVERAGE_COCKTAIL)
|
||||
|
||||
# BEVERAGE_SHOT
|
||||
await catering_service.add_menu_item("Jägermeister", "", Decimal("2.00"), CateringMenuItemCategory.BEVERAGE_SHOT)
|
||||
await catering_service.add_menu_item("Tequila", "", Decimal("2.00"), CateringMenuItemCategory.BEVERAGE_SHOT)
|
||||
await catering_service.add_menu_item("PfEZzi", "Getunter Pfefferminz-Schnaps", Decimal("1.99"),
|
||||
CateringMenuItemCategory.BEVERAGE_SHOT)
|
||||
|
||||
# NON_FOOD
|
||||
await catering_service.add_menu_item("Zigaretten", "Elixyr", Decimal("8.00"), CateringMenuItemCategory.NON_FOOD)
|
||||
await catering_service.add_menu_item("Mentholfilter", "passend für Elixyr", Decimal("1.20"),
|
||||
CateringMenuItemCategory.NON_FOOD)
|
||||
|
||||
if not input("Generate default new post? (Y/n): ").lower() == "n":
|
||||
loops = 0
|
||||
user = None
|
||||
while loops < 1000:
|
||||
user = await user_service.get_user(loops)
|
||||
if user is not None:
|
||||
break
|
||||
loops += 1
|
||||
|
||||
if user is None:
|
||||
sys.exit("Database does not contain users! Exiting...")
|
||||
|
||||
await news_service.add_news(News(
|
||||
news_id=None,
|
||||
title="Der EZGG LAN Manager",
|
||||
subtitle="Eine Software des EZ GG e.V.",
|
||||
content="Dies ist eine WIP-Version des EZGG LAN Managers. Diese Software soll uns helfen in Zukunft die LAN "
|
||||
"Parties des EZ GG e.V.'s zu organisieren. Wer Fehler findet darf sie behalten. (Oder er meldet "
|
||||
"sie)",
|
||||
author=user,
|
||||
news_date=date.today()
|
||||
))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with asyncio.Runner() as loop:
|
||||
loop.run(run())
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -0,0 +1,76 @@
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, ROUND_DOWN
|
||||
from typing import Optional
|
||||
|
||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
||||
from src.ezgg_lan_manager.types.Transaction import Transaction
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
|
||||
class InsufficientFundsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AccountingService:
|
||||
def __init__(self, db_service: DatabaseService) -> None:
|
||||
self._db_service = db_service
|
||||
self._update_hooks: set[Callable] = set()
|
||||
|
||||
def add_update_hook(self, update_hook: Callable) -> None:
|
||||
""" Adds a function to this service, which is called whenever the account balance changes """
|
||||
self._update_hooks.add(update_hook)
|
||||
|
||||
async def add_balance(self, user_id: int, balance_to_add: Decimal, reference: str) -> Decimal:
|
||||
await self._db_service.add_transaction(Transaction(
|
||||
user_id=user_id,
|
||||
value=balance_to_add,
|
||||
is_debit=False,
|
||||
reference=reference,
|
||||
transaction_date=datetime.now()
|
||||
))
|
||||
logger.debug(f"Added balance of {self.make_euro_string_from_decimal(balance_to_add)} to user with ID {user_id}")
|
||||
for update_hook in self._update_hooks:
|
||||
await update_hook()
|
||||
return await self.get_balance(user_id)
|
||||
|
||||
async def remove_balance(self, user_id: int, balance_to_remove: Decimal, reference: str) -> Decimal:
|
||||
current_balance = await self.get_balance(user_id)
|
||||
if (current_balance - balance_to_remove) < 0:
|
||||
raise InsufficientFundsError
|
||||
await self._db_service.add_transaction(Transaction(
|
||||
user_id=user_id,
|
||||
value=balance_to_remove,
|
||||
is_debit=True,
|
||||
reference=reference,
|
||||
transaction_date=datetime.now()
|
||||
))
|
||||
logger.debug(
|
||||
f"Removed balance of {self.make_euro_string_from_decimal(balance_to_remove)} to user with ID {user_id}")
|
||||
for update_hook in self._update_hooks:
|
||||
await update_hook()
|
||||
return await self.get_balance(user_id)
|
||||
|
||||
async def get_balance(self, user_id: int) -> Decimal:
|
||||
balance_buffer = Decimal("0")
|
||||
for transaction in await self._db_service.get_all_transactions_for_user(user_id):
|
||||
if transaction.is_debit:
|
||||
balance_buffer -= transaction.value
|
||||
else:
|
||||
balance_buffer += transaction.value
|
||||
return balance_buffer
|
||||
|
||||
async def get_transaction_history(self, user_id: int) -> list[Transaction]:
|
||||
return await self._db_service.get_all_transactions_for_user(user_id)
|
||||
|
||||
@staticmethod
|
||||
def make_euro_string_from_decimal(euros: Optional[Decimal]) -> str:
|
||||
"""
|
||||
Internally, all money values are euros as decimal. Only when showing them to the user we generate a string.
|
||||
"""
|
||||
if euros is None:
|
||||
return "0.00 €"
|
||||
rounded_decimal = str(euros.quantize(Decimal(".01"), rounding=ROUND_DOWN))
|
||||
return f"{rounded_decimal} €"
|
||||
@@ -0,0 +1,147 @@
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from src.ezgg_lan_manager.services.AccountingService import AccountingService
|
||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
||||
from src.ezgg_lan_manager.services.UserService import UserService
|
||||
from src.ezgg_lan_manager.services.ReceiptPrintingService import ReceiptPrintingService
|
||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus, CateringMenuItemsWithAmount
|
||||
from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItem, CateringMenuItemCategory
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
|
||||
class CateringErrorType(Enum):
|
||||
INCLUDES_DISABLED_ITEM = 0
|
||||
INSUFFICIENT_FUNDS = 1
|
||||
GENERIC = 99
|
||||
|
||||
|
||||
class CateringError(Exception):
|
||||
def __init__(self, message: str, error_type: CateringErrorType = CateringErrorType.GENERIC) -> None:
|
||||
self.message = message
|
||||
self.error_type = error_type
|
||||
|
||||
|
||||
class CateringService:
|
||||
def __init__(self, db_service: DatabaseService, accounting_service: AccountingService, user_service: UserService, receipt_printing_service: ReceiptPrintingService) -> None:
|
||||
self._db_service = db_service
|
||||
self._accounting_service = accounting_service
|
||||
self._user_service = user_service
|
||||
self._receipt_printing_service = receipt_printing_service
|
||||
self.cached_cart: dict[int, list[CateringMenuItem]] = {}
|
||||
|
||||
# ORDERS
|
||||
|
||||
async def place_order(self, menu_items: CateringMenuItemsWithAmount, user_id: int,
|
||||
is_delivery: bool = True) -> CateringOrder:
|
||||
for menu_item in menu_items:
|
||||
if menu_item.is_disabled:
|
||||
raise CateringError("Order includes disabled items", CateringErrorType.INCLUDES_DISABLED_ITEM)
|
||||
|
||||
user = await self._user_service.get_user(user_id)
|
||||
if not user:
|
||||
raise CateringError("User does not exist")
|
||||
|
||||
total_price = sum([item.price * quantity for item, quantity in menu_items.items()], Decimal(0))
|
||||
if await self._accounting_service.get_balance(user_id) < total_price:
|
||||
raise CateringError("Insufficient funds", CateringErrorType.INSUFFICIENT_FUNDS)
|
||||
|
||||
order = await self._db_service.add_new_order(menu_items, user_id, is_delivery)
|
||||
if order:
|
||||
await self._accounting_service.remove_balance(user_id, total_price, f"CATERING - {order.order_id}")
|
||||
logger.info(
|
||||
f"User '{order.customer.user_name}' (ID:{order.customer.user_id}) ordered from catering for {self._accounting_service.make_euro_string_from_decimal(total_price)}")
|
||||
await self._receipt_printing_service.print_order(user, order)
|
||||
# await self.cancel_order(order) # ToDo: Check if commented out before commit. Un-comment to auto-cancel every placed order
|
||||
return order
|
||||
|
||||
async def update_order_status(self, order_id: int, new_status: CateringOrderStatus) -> bool:
|
||||
if new_status == CateringOrderStatus.CANCELED:
|
||||
# Cancelled orders need to be refunded
|
||||
raise CateringError("Orders cannot be canceled this way, use CateringService.cancel_order")
|
||||
return await self._db_service.change_order_status(order_id, new_status)
|
||||
|
||||
async def get_orders(self) -> list[CateringOrder]:
|
||||
return await self._db_service.get_orders()
|
||||
|
||||
async def get_orders_for_user(self, user_id: int) -> list[CateringOrder]:
|
||||
return await self._db_service.get_orders(user_id=user_id)
|
||||
|
||||
async def get_orders_by_status(self, status: CateringOrderStatus) -> list[CateringOrder]:
|
||||
return await self._db_service.get_orders(status=status)
|
||||
|
||||
async def cancel_order(self, order: CateringOrder) -> bool:
|
||||
change_result = await self._db_service.change_order_status(order.order_id, CateringOrderStatus.CANCELED)
|
||||
if change_result:
|
||||
await self._accounting_service.add_balance(order.customer.user_id, order.price,
|
||||
f"CATERING REFUND - {order.order_id}")
|
||||
return True
|
||||
return False
|
||||
|
||||
# MENU ITEMS
|
||||
|
||||
async def get_menu(self, category: Optional[CateringMenuItemCategory] = None) -> list[CateringMenuItem]:
|
||||
items = await self._db_service.get_menu_items()
|
||||
if not category:
|
||||
return items
|
||||
return list(filter(lambda item: item.category == category, items))
|
||||
|
||||
async def get_menu_item_by_id(self, menu_item_id: int) -> CateringMenuItem:
|
||||
item = await self._db_service.get_menu_item(menu_item_id)
|
||||
if not item:
|
||||
raise CateringError("Menu item not found")
|
||||
return item
|
||||
|
||||
async def add_menu_item(self, name: str, info: str, price: Decimal, category: CateringMenuItemCategory,
|
||||
is_disabled: bool = False) -> CateringMenuItem:
|
||||
if new_item := await self._db_service.add_menu_item(name, info, price, category, is_disabled):
|
||||
return new_item
|
||||
raise CateringError(f"Could not add item '{name}' to the menu.")
|
||||
|
||||
async def remove_menu_item(self, menu_item_id: int) -> bool:
|
||||
return await self._db_service.delete_menu_item(menu_item_id)
|
||||
|
||||
async def change_menu_item(self, updated_item: CateringMenuItem) -> bool:
|
||||
return await self._db_service.update_menu_item(updated_item)
|
||||
|
||||
async def disable_menu_item(self, menu_item_id: int) -> bool:
|
||||
try:
|
||||
item = await self.get_menu_item_by_id(menu_item_id)
|
||||
except CateringError:
|
||||
return False
|
||||
item.is_disabled = True
|
||||
return await self._db_service.update_menu_item(item)
|
||||
|
||||
async def enable_menu_item(self, menu_item_id: int) -> bool:
|
||||
try:
|
||||
item = await self.get_menu_item_by_id(menu_item_id)
|
||||
except CateringError:
|
||||
return False
|
||||
item.is_disabled = False
|
||||
return await self._db_service.update_menu_item(item)
|
||||
|
||||
async def disable_menu_items_by_category(self, category: CateringMenuItemCategory) -> bool:
|
||||
items = await self.get_menu(category=category)
|
||||
return all([self.disable_menu_item(item.item_id) for item in items])
|
||||
|
||||
async def enable_menu_items_by_category(self, category: CateringMenuItemCategory) -> bool:
|
||||
items = await self.get_menu(category=category)
|
||||
return all([self.enable_menu_item(item.item_id) for item in items])
|
||||
|
||||
# CART
|
||||
|
||||
def save_cart(self, user_id: Optional[int], cart: list[CateringMenuItem]) -> None:
|
||||
if user_id:
|
||||
self.cached_cart[user_id] = cart
|
||||
|
||||
def get_cart(self, user_id: Optional[int]) -> list[CateringMenuItem]:
|
||||
if user_id is None:
|
||||
return []
|
||||
try:
|
||||
return self.cached_cart[user_id]
|
||||
except KeyError:
|
||||
return []
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
import logging
|
||||
import tomllib
|
||||
|
||||
from from_root import from_root
|
||||
|
||||
from src.ezgg_lan_manager.types.ConfigurationTypes import DatabaseConfiguration, MailingServiceConfiguration, LanInfo, \
|
||||
SeatingConfiguration, TicketInfo, ReceiptPrintingConfiguration
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
|
||||
class ConfigurationService:
|
||||
def __init__(self, config_file_path: Path) -> None:
|
||||
try:
|
||||
with open(from_root("VERSION"), "r") as version_file:
|
||||
self._version = version_file.read().strip()
|
||||
except FileNotFoundError:
|
||||
logger.warning("Could not find VERSION file, defaulting to '0.0.0'")
|
||||
self._version = "0.0.0"
|
||||
|
||||
try:
|
||||
with open(config_file_path, "rb") as config_file:
|
||||
self._config = tomllib.load(config_file)
|
||||
except FileNotFoundError:
|
||||
logger.fatal(f"Could not find config file at \"{config_file_path}\", exiting...")
|
||||
exit(1)
|
||||
|
||||
def get_database_configuration(self) -> DatabaseConfiguration:
|
||||
try:
|
||||
database_configuration = self._config["database"]
|
||||
return DatabaseConfiguration(
|
||||
db_user=database_configuration["db_user"],
|
||||
db_password=database_configuration["db_password"],
|
||||
db_host=database_configuration["db_host"],
|
||||
db_port=database_configuration["db_port"],
|
||||
db_name=database_configuration["db_name"]
|
||||
)
|
||||
except KeyError:
|
||||
logger.fatal("Error loading DatabaseConfiguration, exiting...")
|
||||
sys.exit(1)
|
||||
|
||||
def get_mailing_service_configuration(self) -> MailingServiceConfiguration:
|
||||
try:
|
||||
mailing_configuration = self._config["mailing"]
|
||||
return MailingServiceConfiguration(
|
||||
smtp_server=mailing_configuration["smtp_server"],
|
||||
smtp_port=mailing_configuration["smtp_port"],
|
||||
sender=mailing_configuration["sender"],
|
||||
username=mailing_configuration["username"],
|
||||
password=mailing_configuration["password"]
|
||||
)
|
||||
except KeyError:
|
||||
logger.fatal("Error loading MailingServiceConfiguration, exiting...")
|
||||
sys.exit(1)
|
||||
|
||||
def get_lan_info(self) -> LanInfo:
|
||||
try:
|
||||
lan_info = self._config["lan"]
|
||||
return LanInfo(
|
||||
name=lan_info["name"],
|
||||
iteration=lan_info["iteration"],
|
||||
date_from=datetime.strptime(lan_info["date_from"], "%Y-%m-%d %H:%M:%S"),
|
||||
date_till=datetime.strptime(lan_info["date_till"], "%Y-%m-%d %H:%M:%S"),
|
||||
organizer_mail=lan_info["organizer_mail"]
|
||||
)
|
||||
except KeyError:
|
||||
logger.fatal("Error loading LAN Info, exiting...")
|
||||
sys.exit(1)
|
||||
|
||||
def get_ticket_info(self) -> tuple[TicketInfo, ...]:
|
||||
try:
|
||||
return tuple([TicketInfo(
|
||||
category=value,
|
||||
total_tickets=self._config["tickets"][value]["total_tickets"],
|
||||
price=Decimal(self._config["tickets"][value]["price"]),
|
||||
description=self._config["tickets"][value]["description"],
|
||||
additional_info=self._config["tickets"][value]["additional_info"],
|
||||
is_default=self._config["tickets"][value]["is_default"]
|
||||
) for value in self._config["tickets"]])
|
||||
except KeyError as e:
|
||||
logger.debug(e)
|
||||
logger.fatal("Error loading seating configuration, exiting...")
|
||||
sys.exit(1)
|
||||
|
||||
def get_receipt_printing_configuration(self) -> ReceiptPrintingConfiguration:
|
||||
try:
|
||||
receipt_printing_configuration = self._config["receipt_printing"]
|
||||
return ReceiptPrintingConfiguration(
|
||||
host=receipt_printing_configuration["host"],
|
||||
port=receipt_printing_configuration["port"],
|
||||
order_print_endpoint=receipt_printing_configuration["order_print_endpoint"],
|
||||
password=receipt_printing_configuration["password"]
|
||||
)
|
||||
except KeyError:
|
||||
logger.fatal("Error loading Receipt Printing Configuration, exiting...")
|
||||
sys.exit(1)
|
||||
|
||||
@property
|
||||
def APP_VERSION(self) -> str:
|
||||
return self._version
|
||||
|
||||
@property
|
||||
def DEV_MODE_ACTIVE(self) -> bool:
|
||||
return self._config["misc"]["dev_mode_active"]
|
||||
@@ -0,0 +1,789 @@
|
||||
import logging
|
||||
|
||||
from datetime import date, datetime
|
||||
from typing import Optional
|
||||
from decimal import Decimal
|
||||
|
||||
import aiomysql
|
||||
|
||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder
|
||||
from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItem, CateringMenuItemCategory
|
||||
from src.ezgg_lan_manager.types.CateringOrder import CateringMenuItemsWithAmount, CateringOrderStatus
|
||||
from src.ezgg_lan_manager.types.ConfigurationTypes import DatabaseConfiguration
|
||||
from src.ezgg_lan_manager.types.News import News
|
||||
from src.ezgg_lan_manager.types.Seat import Seat
|
||||
from src.ezgg_lan_manager.types.Ticket import Ticket
|
||||
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 DuplicationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoDatabaseConnectionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseService:
|
||||
MAX_CONNECTION_RETRIES = 5
|
||||
|
||||
def __init__(self, database_config: DatabaseConfiguration) -> None:
|
||||
self._database_config = database_config
|
||||
self._connection_pool: Optional[aiomysql.Pool] = None
|
||||
|
||||
async def is_healthy(self) -> bool:
|
||||
try:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor() as _:
|
||||
return True
|
||||
except aiomysql.OperationalError:
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to acquire a connection: {e}")
|
||||
return False
|
||||
|
||||
async def init_db_pool(self) -> bool:
|
||||
logger.info(
|
||||
f"Connecting to database '{self._database_config.db_name}' on "
|
||||
f"{self._database_config.db_user}@{self._database_config.db_host}:{self._database_config.db_port}"
|
||||
)
|
||||
try:
|
||||
self._connection_pool = await aiomysql.create_pool(
|
||||
host=self._database_config.db_host,
|
||||
port=self._database_config.db_port,
|
||||
user=self._database_config.db_user,
|
||||
password=self._database_config.db_password,
|
||||
db=self._database_config.db_name,
|
||||
minsize=1,
|
||||
maxsize=40
|
||||
)
|
||||
except aiomysql.OperationalError:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _map_db_result_to_user(data: tuple) -> User:
|
||||
return User(
|
||||
user_id=data[0],
|
||||
user_name=data[1],
|
||||
user_mail=data[2],
|
||||
user_password=data[3],
|
||||
user_first_name=data[4],
|
||||
user_last_name=data[5],
|
||||
user_birth_day=data[6],
|
||||
is_active=bool(data[7]),
|
||||
is_team_member=bool(data[8]),
|
||||
is_admin=bool(data[9]),
|
||||
created_at=data[10],
|
||||
last_updated_at=data[11]
|
||||
)
|
||||
|
||||
async def get_user_by_name(self, user_name: str) -> Optional[User]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
await cursor.execute("SELECT * FROM users WHERE user_name=%s", (user_name,))
|
||||
result = await cursor.fetchone()
|
||||
if not result:
|
||||
return
|
||||
return self._map_db_result_to_user(result)
|
||||
|
||||
async def get_user_by_id(self, user_id: int) -> Optional[User]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
await cursor.execute("SELECT * FROM users WHERE user_id=%s", (user_id,))
|
||||
result = await cursor.fetchone()
|
||||
if not result:
|
||||
return
|
||||
return self._map_db_result_to_user(result)
|
||||
|
||||
async def get_user_by_mail(self, user_mail: str) -> Optional[User]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
await cursor.execute("SELECT * FROM users WHERE user_mail=%s", (user_mail.lower(),))
|
||||
result = await cursor.fetchone()
|
||||
if not result:
|
||||
return
|
||||
return self._map_db_result_to_user(result)
|
||||
|
||||
async def create_user(self, user_name: str, user_mail: str, password_hash: str) -> User:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"INSERT INTO users (user_name, user_mail, user_password) "
|
||||
"VALUES (%s, %s, %s)", (user_name, user_mail.lower(), password_hash)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.create_user(user_name, user_mail, password_hash)
|
||||
except aiomysql.IntegrityError as e:
|
||||
logger.warning(f"Aborted duplication entry: {e}")
|
||||
raise DuplicationError
|
||||
|
||||
return await self.get_user_by_name(user_name)
|
||||
|
||||
async def update_user(self, user: User) -> User:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"UPDATE users SET user_name=%s, user_mail=%s, user_password=%s, user_first_name=%s, "
|
||||
"user_last_name=%s, user_birth_date=%s, is_active=%s, is_team_member=%s, is_admin=%s "
|
||||
"WHERE (user_id=%s)",
|
||||
(user.user_name, user.user_mail.lower(), user.user_password,
|
||||
user.user_first_name, user.user_last_name, user.user_birth_day,
|
||||
user.is_active, user.is_team_member, user.is_admin,
|
||||
user.user_id)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.update_user(user)
|
||||
except aiomysql.IntegrityError as e:
|
||||
logger.warning(f"Aborted duplication entry: {e}")
|
||||
raise DuplicationError
|
||||
return user
|
||||
|
||||
async def add_transaction(self, transaction: Transaction) -> Optional[Transaction]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"INSERT INTO transactions (user_id, value, is_debit, transaction_date, transaction_reference) "
|
||||
"VALUES (%s, %s, %s, %s, %s)",
|
||||
(transaction.user_id, transaction.value, transaction.is_debit, transaction.transaction_date,
|
||||
transaction.reference)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.add_transaction(transaction)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error adding Transaction: {e}")
|
||||
return
|
||||
|
||||
return transaction
|
||||
|
||||
async def get_all_transactions_for_user(self, user_id: int) -> list[Transaction]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
transactions = []
|
||||
try:
|
||||
await cursor.execute("SELECT * FROM transactions WHERE user_id=%s", (user_id,))
|
||||
await conn.commit()
|
||||
result = await cursor.fetchall()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_all_transactions_for_user(user_id)
|
||||
except aiomysql.Error as e:
|
||||
logger.error(f"Error getting all transactions for user: {e}")
|
||||
return []
|
||||
|
||||
for transaction_raw in result:
|
||||
transactions.append(Transaction(
|
||||
user_id=user_id,
|
||||
value=Decimal(transaction_raw[2]),
|
||||
is_debit=bool(transaction_raw[3]),
|
||||
transaction_date=transaction_raw[4],
|
||||
reference=transaction_raw[5]
|
||||
))
|
||||
return transactions
|
||||
|
||||
async def add_news(self, news: News) -> None:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"INSERT INTO news (news_content, news_title, news_subtitle, news_author, news_date) "
|
||||
"VALUES (%s, %s, %s, %s, %s)",
|
||||
(news.content, news.title, news.subtitle, news.author.user_id, news.news_date)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.add_news(news)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error adding Transaction: {e}")
|
||||
|
||||
async def get_news(self, dt_start: date, dt_end: date) -> list[News]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
results = []
|
||||
try:
|
||||
await cursor.execute(
|
||||
"SELECT * FROM news INNER JOIN users ON news.news_author = users.user_id WHERE news_date"
|
||||
" BETWEEN %s AND %s;",
|
||||
(dt_start, dt_end))
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_news(dt_start, dt_end)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching news: {e}")
|
||||
return []
|
||||
|
||||
for news_raw in await cursor.fetchall():
|
||||
user = self._map_db_result_to_user(news_raw[6:])
|
||||
results.append(News(
|
||||
news_id=news_raw[0],
|
||||
title=news_raw[2],
|
||||
subtitle=news_raw[3],
|
||||
author=user,
|
||||
content=news_raw[1],
|
||||
news_date=news_raw[5]
|
||||
))
|
||||
|
||||
return results
|
||||
|
||||
async def update_news(self, news: News) -> None:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"""
|
||||
UPDATE news
|
||||
SET news_content = %s,
|
||||
news_title = %s,
|
||||
news_subtitle = %s,
|
||||
news_author = %s,
|
||||
news_date = %s
|
||||
WHERE news_id = %s
|
||||
""",
|
||||
(news.content, news.title, news.subtitle, news.author.user_id, news.news_date, news.news_id)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.update_news(news)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error updating news: {e}")
|
||||
|
||||
async def remove_news(self, news_id: int) -> None:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"DELETE FROM news WHERE news_id = %s",
|
||||
(news_id,)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.remove_news(news_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error removing news with ID {news_id}: {e}")
|
||||
|
||||
async def get_tickets(self) -> list[Ticket]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
results = []
|
||||
try:
|
||||
await cursor.execute("SELECT * FROM tickets INNER JOIN users ON tickets.user = users.user_id;", ())
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_tickets()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching tickets: {e}")
|
||||
return []
|
||||
|
||||
for ticket_raw in await cursor.fetchall():
|
||||
user = self._map_db_result_to_user(ticket_raw[3:])
|
||||
results.append(Ticket(
|
||||
ticket_id=ticket_raw[0],
|
||||
category=ticket_raw[1],
|
||||
purchase_date=ticket_raw[3],
|
||||
owner=user
|
||||
))
|
||||
|
||||
return results
|
||||
|
||||
async def get_ticket_for_user(self, user_id: int) -> Optional[Ticket]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"SELECT * FROM tickets INNER JOIN users ON tickets.user = users.user_id WHERE user_id=%s;",
|
||||
(user_id,))
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_ticket_for_user(user_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching ticket for user: {e}")
|
||||
return
|
||||
|
||||
result = await cursor.fetchone()
|
||||
if not result:
|
||||
return
|
||||
|
||||
user = self._map_db_result_to_user(result[3:])
|
||||
return Ticket(
|
||||
ticket_id=result[0],
|
||||
category=result[1],
|
||||
purchase_date=result[3],
|
||||
owner=user
|
||||
)
|
||||
|
||||
async def generate_ticket_for_user(self, user_id: int, category: str) -> Optional[Ticket]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute("INSERT INTO tickets (ticket_category, user) VALUES (%s, %s)",
|
||||
(category, user_id))
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.generate_ticket_for_user(user_id, category)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error generating ticket for user: {e}")
|
||||
return
|
||||
|
||||
return await self.get_ticket_for_user(user_id)
|
||||
|
||||
async def change_ticket_owner(self, ticket_id: int, new_owner_id: int) -> bool:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute("UPDATE tickets SET user = %s WHERE ticket_id = %s;",
|
||||
(new_owner_id, ticket_id))
|
||||
affected_rows = cursor.rowcount
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.change_ticket_owner(ticket_id, new_owner_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error transferring ticket to user: {e}")
|
||||
return False
|
||||
return affected_rows > 0
|
||||
|
||||
async def delete_ticket(self, ticket_id: int) -> bool:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute("DELETE FROM tickets WHERE ticket_id = %s;", (ticket_id,))
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.change_ticket_owner(ticket_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error deleting ticket: {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
async def generate_fresh_seats_table(self, seats: list[tuple[str, str]]) -> None:
|
||||
""" WARNING: THIS WILL DELETE ALL EXISTING DATA! DO NOT USE ON PRODUCTION DATABASE! """
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute("TRUNCATE seats;")
|
||||
for seat in seats:
|
||||
await cursor.execute("INSERT INTO seats (seat_id, seat_category) VALUES (%s, %s);",
|
||||
(seat[0], seat[1]))
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.generate_fresh_seats_table(seats)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error generating fresh seats table: {e}")
|
||||
return
|
||||
|
||||
async def get_seating_info(self) -> list[Seat]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
results = []
|
||||
try:
|
||||
await cursor.execute(
|
||||
"SELECT seats.*, users.* FROM seats LEFT JOIN users ON seats.user = users.user_id;")
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_seating_info()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting seats table: {e}")
|
||||
return results
|
||||
|
||||
for seat_raw in await cursor.fetchall():
|
||||
if seat_raw[3] is None: # Empty seat
|
||||
results.append(Seat(seat_raw[0], bool(seat_raw[1]), seat_raw[2], None))
|
||||
else:
|
||||
user = self._map_db_result_to_user(seat_raw[4:])
|
||||
results.append(Seat(seat_raw[0], bool(seat_raw[1]), seat_raw[2], user))
|
||||
|
||||
return results
|
||||
|
||||
async def seat_user(self, seat_id: str, user_id: int) -> bool:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute("UPDATE seats SET user = %s WHERE seat_id = %s;", (user_id, seat_id))
|
||||
affected_rows = cursor.rowcount
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.seat_user(seat_id, user_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error seating user: {e}")
|
||||
return False
|
||||
return affected_rows > 0
|
||||
|
||||
async def get_menu_items(self) -> list[CateringMenuItem]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
results = []
|
||||
try:
|
||||
await cursor.execute("SELECT * FROM catering_menu_items;")
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_menu_items()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching menu items: {e}")
|
||||
return results
|
||||
|
||||
for menu_item_raw in await cursor.fetchall():
|
||||
results.append(CateringMenuItem(
|
||||
item_id=menu_item_raw[0],
|
||||
name=menu_item_raw[1],
|
||||
additional_info=menu_item_raw[2],
|
||||
price=Decimal(menu_item_raw[3]),
|
||||
category=CateringMenuItemCategory(menu_item_raw[4]),
|
||||
is_disabled=bool(menu_item_raw[5])
|
||||
))
|
||||
|
||||
return results
|
||||
|
||||
async def get_menu_item(self, menu_item_id) -> Optional[CateringMenuItem]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute("SELECT * FROM catering_menu_items WHERE catering_menu_item_id = %s;",
|
||||
(menu_item_id,))
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_menu_item(menu_item_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching menu items: {e}")
|
||||
return
|
||||
|
||||
raw_data = await cursor.fetchone()
|
||||
if raw_data is None:
|
||||
return
|
||||
return CateringMenuItem(
|
||||
item_id=raw_data[0],
|
||||
name=raw_data[1],
|
||||
additional_info=raw_data[2],
|
||||
price=Decimal(raw_data[3]),
|
||||
category=CateringMenuItemCategory(raw_data[4]),
|
||||
is_disabled=bool(raw_data[5])
|
||||
)
|
||||
|
||||
async def add_menu_item(self, name: str, info: str, price: Decimal, category: CateringMenuItemCategory,
|
||||
is_disabled: bool = False) -> Optional[CateringMenuItem]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"INSERT INTO catering_menu_items (name, additional_info, price, category, is_disabled) VALUES "
|
||||
"(%s, %s, %s, %s, %s);",
|
||||
(name, info, price, category.value, is_disabled)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.add_menu_item(name, info, price, category, is_disabled)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error adding menu item: {e}")
|
||||
return
|
||||
|
||||
return CateringMenuItem(
|
||||
item_id=cursor.lastrowid,
|
||||
name=name,
|
||||
additional_info=info,
|
||||
price=price,
|
||||
category=category,
|
||||
is_disabled=is_disabled
|
||||
)
|
||||
|
||||
async def delete_menu_item(self, menu_item_id: int) -> bool:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute("DELETE FROM catering_menu_items WHERE catering_menu_item_id = %s;",
|
||||
(menu_item_id,))
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.delete_menu_item(menu_item_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error deleting menu item: {e}")
|
||||
return False
|
||||
return cursor.affected_rows > 0
|
||||
|
||||
async def update_menu_item(self, updated_item: CateringMenuItem) -> bool:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"UPDATE catering_menu_items SET name = %s, additional_info = %s, price = %s, category = %s, "
|
||||
"is_disabled = %s WHERE catering_menu_item_id = %s;",
|
||||
(updated_item.name, updated_item.additional_info, updated_item.price,
|
||||
updated_item.category.value, updated_item.is_disabled, updated_item.item_id)
|
||||
)
|
||||
affected_rows = cursor.rowcount
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.update_menu_item(updated_item)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error updating menu item: {e}")
|
||||
return False
|
||||
return affected_rows > 0
|
||||
|
||||
async def add_new_order(self, menu_items: CateringMenuItemsWithAmount, user_id: int, is_delivery: bool) -> Optional[CateringOrder]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
now = datetime.now()
|
||||
try:
|
||||
await cursor.execute(
|
||||
"INSERT INTO orders (status, user, is_delivery, order_date) VALUES (%s, %s, %s, %s);",
|
||||
(CateringOrderStatus.RECEIVED.value, user_id, is_delivery, now)
|
||||
)
|
||||
order_id = cursor.lastrowid
|
||||
for menu_item, quantity in menu_items.items():
|
||||
await cursor.execute(
|
||||
"INSERT INTO order_catering_menu_item (order_id, catering_menu_item_id, quantity) VALUES "
|
||||
"(%s, %s, %s);",
|
||||
(order_id, menu_item.item_id, quantity)
|
||||
)
|
||||
await conn.commit()
|
||||
return CateringOrder(
|
||||
order_id=order_id,
|
||||
order_date=now,
|
||||
status=CateringOrderStatus.RECEIVED,
|
||||
items=menu_items,
|
||||
customer=await self.get_user_by_id(user_id),
|
||||
is_delivery=is_delivery
|
||||
)
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.add_new_order(menu_items, user_id, is_delivery)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error placing order: {e}")
|
||||
return
|
||||
|
||||
async def change_order_status(self, order_id: int, status: CateringOrderStatus) -> bool:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"UPDATE orders SET status = %s WHERE order_id = %s;",
|
||||
(status.value, order_id)
|
||||
)
|
||||
affected_rows = cursor.rowcount
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.change_order_status(order_id, status)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error updating menu item: {e}")
|
||||
return False
|
||||
return affected_rows > 0
|
||||
|
||||
async def get_orders(self, user_id: Optional[int] = None, status: Optional[CateringOrderStatus] = None) -> list[CateringOrder]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
fetched_orders = []
|
||||
query = "SELECT * FROM orders LEFT JOIN users ON orders.user = users.user_id"
|
||||
if user_id is not None and status is None:
|
||||
query += f" WHERE user = {user_id};"
|
||||
elif status is not None and user_id is None:
|
||||
query += f" WHERE status = '{status.value}';"
|
||||
elif status is not None and user_id is not None:
|
||||
query += f" WHERE user = {user_id} AND status = '{status.value}';"
|
||||
else:
|
||||
query += ";"
|
||||
try:
|
||||
await cursor.execute(query)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_orders(user_id, status)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting orders: {e}")
|
||||
return fetched_orders
|
||||
|
||||
for raw_order in await cursor.fetchall():
|
||||
fetched_orders.append(
|
||||
CateringOrder(
|
||||
order_id=raw_order[0],
|
||||
status=CateringOrderStatus(raw_order[1]),
|
||||
customer=self._map_db_result_to_user(raw_order[5:]),
|
||||
items=await self.get_menu_items_for_order(raw_order[0]),
|
||||
is_delivery=bool(raw_order[4]),
|
||||
order_date=raw_order[3],
|
||||
)
|
||||
)
|
||||
|
||||
return fetched_orders
|
||||
|
||||
async def get_menu_items_for_order(self, order_id: int) -> CateringMenuItemsWithAmount:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
result = {}
|
||||
try:
|
||||
await cursor.execute(
|
||||
"SELECT * FROM order_catering_menu_item "
|
||||
"LEFT JOIN catering_menu_items ON order_catering_menu_item.catering_menu_item_id = catering_menu_items.catering_menu_item_id "
|
||||
"WHERE order_id = %s;",
|
||||
(order_id,)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_menu_items_for_order(order_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting order items: {e}")
|
||||
return result
|
||||
|
||||
for order_catering_menu_item_raw in await cursor.fetchall():
|
||||
result[CateringMenuItem(
|
||||
item_id=order_catering_menu_item_raw[1],
|
||||
name=order_catering_menu_item_raw[4],
|
||||
additional_info=order_catering_menu_item_raw[5],
|
||||
price=Decimal(order_catering_menu_item_raw[6]),
|
||||
category=CateringMenuItemCategory(order_catering_menu_item_raw[7]),
|
||||
is_disabled=bool(order_catering_menu_item_raw[8])
|
||||
)] = order_catering_menu_item_raw[2]
|
||||
|
||||
return result
|
||||
|
||||
async def set_user_profile_picture(self, user_id: int, picture_data: bytes) -> None:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"INSERT INTO user_profile_picture (user_id, picture) VALUES (%s, %s) ON DUPLICATE KEY UPDATE picture = VALUES(picture)",
|
||||
(user_id, picture_data)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.set_user_profile_picture(user_id, picture_data)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error setting user profile picture: {e}")
|
||||
|
||||
async def get_user_profile_picture(self, user_id: int) -> Optional[bytes]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute("SELECT (picture) FROM user_profile_picture WHERE user_id = %s", (user_id,))
|
||||
await conn.commit()
|
||||
r = await cursor.fetchone()
|
||||
if r is None:
|
||||
return
|
||||
return r[0]
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_user_profile_picture(user_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error setting user profile picture: {e}")
|
||||
return None
|
||||
|
||||
async def get_all_users(self) -> list[User]:
|
||||
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
results = []
|
||||
try:
|
||||
await cursor.execute("SELECT * FROM users;")
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_all_users()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting all users: {e}")
|
||||
return results
|
||||
|
||||
for user_raw in await cursor.fetchall():
|
||||
results.append(self._map_db_result_to_user(user_raw))
|
||||
|
||||
return results
|
||||
|
||||
async def remove_profile_picture(self, user_id: int):
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"DELETE FROM user_profile_picture WHERE user_id = %s",
|
||||
user_id
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.remove_profile_picture(user_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error deleting user profile picture: {e}")
|
||||
@@ -0,0 +1,25 @@
|
||||
import secrets
|
||||
from typing import Optional
|
||||
|
||||
from rio import UserSettings
|
||||
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
|
||||
|
||||
class LocalData(UserSettings):
|
||||
stored_session_token: Optional[str] = None
|
||||
|
||||
class LocalDataService:
|
||||
def __init__(self) -> None:
|
||||
self._session: dict[str, SessionStorage] = {}
|
||||
|
||||
def verify_token(self, token: str) -> Optional[SessionStorage]:
|
||||
return self._session.get(token)
|
||||
|
||||
def set_session(self, session: SessionStorage) -> str:
|
||||
key = secrets.token_hex(32)
|
||||
self._session[key] = session
|
||||
return key
|
||||
|
||||
def del_session(self, token: str) -> None:
|
||||
self._session.pop(token, None)
|
||||
@@ -0,0 +1,37 @@
|
||||
import logging
|
||||
from email.message import EmailMessage
|
||||
from asyncio import sleep
|
||||
|
||||
import aiosmtplib
|
||||
|
||||
from src.ezgg_lan_manager.services.ConfigurationService import ConfigurationService
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
class MailingService:
|
||||
def __init__(self, configuration_service: ConfigurationService):
|
||||
self._configuration_service = configuration_service
|
||||
self._config = self._configuration_service.get_mailing_service_configuration()
|
||||
|
||||
async def send_email(self, subject: str, body: str, receiver: str) -> None:
|
||||
if self._configuration_service.DEV_MODE_ACTIVE:
|
||||
logger.info(f"Skipped sending mail to {receiver} because demo mode is active.")
|
||||
await sleep(1)
|
||||
return
|
||||
|
||||
try:
|
||||
message = EmailMessage()
|
||||
message["From"] = self._config.sender
|
||||
message["To"] = receiver
|
||||
message["Subject"] = subject
|
||||
message.set_content(body)
|
||||
|
||||
await aiosmtplib.send(
|
||||
message,
|
||||
hostname=self._config.smtp_server,
|
||||
port=self._config.smtp_port,
|
||||
username=self._config.username,
|
||||
password=self._config.password
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send email: {e}")
|
||||
@@ -0,0 +1,39 @@
|
||||
import logging
|
||||
from datetime import date
|
||||
from typing import Optional
|
||||
|
||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
||||
from src.ezgg_lan_manager.types.News import News
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
class NewsService:
|
||||
def __init__(self, db_service: DatabaseService) -> None:
|
||||
self._db_service = db_service
|
||||
|
||||
async def add_news(self, news: News) -> None:
|
||||
if news.news_id is not None:
|
||||
logger.warning("Can not add news with ID, ignoring...")
|
||||
return
|
||||
await self._db_service.add_news(news)
|
||||
|
||||
async def get_news(self, dt_start: Optional[date] = None, dt_end: Optional[date] = None, newest_first: bool = True) -> list[News]:
|
||||
if not dt_end:
|
||||
dt_end = date.today()
|
||||
if not dt_start:
|
||||
dt_start = date(1900, 1, 1)
|
||||
fetched_news = await self._db_service.get_news(dt_start, dt_end)
|
||||
return sorted(fetched_news, key=lambda news: news.news_date, reverse=newest_first)
|
||||
|
||||
async def update_news(self, news: News) -> None:
|
||||
return await self._db_service.update_news(news)
|
||||
|
||||
async def delete_news(self, news_id: int) -> None:
|
||||
return await self._db_service.remove_news(news_id)
|
||||
|
||||
async def get_latest_news(self) -> Optional[News]:
|
||||
try:
|
||||
all_news = await self.get_news(None, date.today())
|
||||
return all_news[0]
|
||||
except IndexError:
|
||||
logger.debug("There are no news to fetch")
|
||||
@@ -0,0 +1,48 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from src.ezgg_lan_manager.services.SeatingService import SeatingService
|
||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder
|
||||
from src.ezgg_lan_manager.types.ConfigurationTypes import ReceiptPrintingConfiguration
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
logging.getLogger("urllib3").setLevel(logging.FATAL) # Disable logging for urllib3
|
||||
|
||||
class ReceiptPrintingService:
|
||||
def __init__(self, seating_service: SeatingService, config: ReceiptPrintingConfiguration, dev_mode_enabled: bool) -> None:
|
||||
self._seating_service = seating_service
|
||||
self._config = config
|
||||
self._dev_mode_enabled = dev_mode_enabled
|
||||
|
||||
async def print_order(self, user: User, order: CateringOrder) -> None:
|
||||
seat_id = await self._seating_service.get_user_seat(user.user_id)
|
||||
if not seat_id:
|
||||
seat_id = " - "
|
||||
|
||||
menu_items_payload = []
|
||||
for item, amount in order.items.items():
|
||||
menu_items_payload.append({
|
||||
"menu_item_name": item.name,
|
||||
"amount": amount
|
||||
})
|
||||
|
||||
payload = {
|
||||
"order_id": str(order.order_id),
|
||||
"order_date": order.order_date.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z',
|
||||
"customer_name": user.user_name,
|
||||
"seat_id": seat_id,
|
||||
"items": menu_items_payload
|
||||
}
|
||||
try:
|
||||
requests.post(
|
||||
f"http://{self._config.host}:{self._config.port}/{self._config.order_print_endpoint}",
|
||||
json=payload,
|
||||
headers={"x-password": self._config.password}
|
||||
)
|
||||
except Exception as e:
|
||||
if self._dev_mode_enabled:
|
||||
logger.info("An error occurred trying to print a receipt:", e)
|
||||
return
|
||||
logger.error("An error occurred trying to print a receipt:", e)
|
||||
@@ -0,0 +1,62 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
|
||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
||||
from src.ezgg_lan_manager.services.TicketingService import TicketingService
|
||||
from src.ezgg_lan_manager.types.ConfigurationTypes import LanInfo, SeatingConfiguration
|
||||
from src.ezgg_lan_manager.types.Seat import Seat
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
class NoTicketError(Exception):
|
||||
pass
|
||||
|
||||
class SeatNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
class WrongCategoryError(Exception):
|
||||
pass
|
||||
|
||||
class SeatAlreadyTakenError(Exception):
|
||||
pass
|
||||
|
||||
class SeatingService:
|
||||
def __init__(self, lan_info: LanInfo, db_service: DatabaseService, ticketing_service: TicketingService) -> None:
|
||||
self._lan_info = lan_info
|
||||
self._db_service = db_service
|
||||
self._ticketing_service = ticketing_service
|
||||
|
||||
async def get_seating(self) -> list[Seat]:
|
||||
return await self._db_service.get_seating_info()
|
||||
|
||||
async def get_seat(self, seat_id: str, cached_data: Optional[list[Seat]] = None) -> Optional[Seat]:
|
||||
all_seats = await self.get_seating() if not cached_data else cached_data
|
||||
for seat in all_seats:
|
||||
if seat.seat_id == seat_id:
|
||||
return seat
|
||||
|
||||
async def get_user_seat(self, user_id: int) -> Optional[Seat]:
|
||||
all_seats = await self.get_seating()
|
||||
for seat in all_seats:
|
||||
if seat.user and seat.user.user_id == user_id:
|
||||
return seat
|
||||
|
||||
async def seat_user(self, user_id: int, seat_id: str) -> None:
|
||||
user_ticket = await self._ticketing_service.get_user_ticket(user_id)
|
||||
if not user_ticket:
|
||||
raise NoTicketError
|
||||
|
||||
seat = await self.get_seat(seat_id)
|
||||
if not seat:
|
||||
raise SeatNotFoundError
|
||||
|
||||
if seat.category != user_ticket.category:
|
||||
raise WrongCategoryError
|
||||
|
||||
if seat.user is not None:
|
||||
raise SeatAlreadyTakenError
|
||||
|
||||
await self._db_service.seat_user(seat_id, user_id)
|
||||
|
||||
# ToDo: Make function that creates database table `seats` from config
|
||||
@@ -0,0 +1,93 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from src.ezgg_lan_manager.services.AccountingService import AccountingService, InsufficientFundsError
|
||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
||||
from src.ezgg_lan_manager.types.ConfigurationTypes import TicketInfo
|
||||
from src.ezgg_lan_manager.types.Ticket import Ticket
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
|
||||
class TicketNotAvailableError(Exception):
|
||||
def __init__(self, category: str):
|
||||
self.category = category
|
||||
|
||||
|
||||
class UserAlreadyHasTicketError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TicketingService:
|
||||
def __init__(self, ticket_infos: tuple[TicketInfo, ...], db_service: DatabaseService,
|
||||
accounting_service: AccountingService) -> None:
|
||||
self._ticket_infos = ticket_infos
|
||||
self._db_service = db_service
|
||||
self._accounting_service = accounting_service
|
||||
|
||||
def get_ticket_info_by_category(self, category: str) -> Optional[TicketInfo]:
|
||||
return next(filter(lambda t: t.category == category, self._ticket_infos), None)
|
||||
|
||||
def get_total_tickets(self) -> int:
|
||||
return sum([t_i.total_tickets for t_i in self._ticket_infos])
|
||||
|
||||
async def get_available_tickets_for_category(self, category: str) -> int:
|
||||
ticket_info = self.get_ticket_info_by_category(category)
|
||||
if not ticket_info or ticket_info.total_tickets < 1:
|
||||
return 0
|
||||
result = ticket_info.total_tickets
|
||||
|
||||
all_tickets = await self._db_service.get_tickets()
|
||||
for ticket in all_tickets:
|
||||
if ticket.category == category:
|
||||
result -= 1
|
||||
|
||||
return result
|
||||
|
||||
async def purchase_ticket(self, user_id: int, category: str) -> Ticket:
|
||||
all_categories = [t_i.category for t_i in self._ticket_infos]
|
||||
if category not in all_categories or (await self.get_available_tickets_for_category(category)) < 1:
|
||||
raise TicketNotAvailableError(category)
|
||||
|
||||
user_balance = await self._accounting_service.get_balance(user_id)
|
||||
|
||||
ticket_info = self.get_ticket_info_by_category(category)
|
||||
if not ticket_info:
|
||||
raise TicketNotAvailableError(category)
|
||||
|
||||
if ticket_info.price > user_balance:
|
||||
raise InsufficientFundsError
|
||||
|
||||
if await self.get_user_ticket(user_id):
|
||||
raise UserAlreadyHasTicketError
|
||||
|
||||
if new_ticket := await self._db_service.generate_ticket_for_user(user_id, category):
|
||||
await self._accounting_service.remove_balance(
|
||||
user_id,
|
||||
ticket_info.price,
|
||||
f"TICKET {new_ticket.ticket_id}"
|
||||
)
|
||||
logger.debug(f"User {user_id} purchased ticket {new_ticket.ticket_id}")
|
||||
return new_ticket
|
||||
|
||||
raise RuntimeError("An unknown error occurred while purchasing ticket")
|
||||
|
||||
async def refund_ticket(self, user_id: int) -> bool:
|
||||
user_ticket = await self.get_user_ticket(user_id)
|
||||
if not user_ticket:
|
||||
return False
|
||||
|
||||
ticket_info = self.get_ticket_info_by_category(user_ticket.category)
|
||||
if await self._db_service.delete_ticket(user_ticket.ticket_id):
|
||||
await self._accounting_service.add_balance(user_id, ticket_info.price,
|
||||
f"TICKET REFUND {user_ticket.ticket_id}")
|
||||
logger.debug(f"User {user_id} refunded ticket {user_ticket.ticket_id}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def transfer_ticket(self, ticket_id: int, user_id: int) -> bool:
|
||||
return await self._db_service.change_ticket_owner(ticket_id, user_id)
|
||||
|
||||
async def get_user_ticket(self, user_id: int) -> Optional[Ticket]:
|
||||
return await self._db_service.get_ticket_for_user(user_id)
|
||||
@@ -0,0 +1,70 @@
|
||||
from hashlib import sha256
|
||||
from typing import Union, Optional
|
||||
from string import ascii_letters, digits
|
||||
|
||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
|
||||
class NameNotAllowedError(Exception):
|
||||
def __init__(self, disallowed_char: str) -> None:
|
||||
self.disallowed_char = disallowed_char
|
||||
|
||||
class UserService:
|
||||
ALLOWED_USER_NAME_SYMBOLS = ascii_letters + digits + "!#$%&*+,-./:;<=>?[]^_{|}~"
|
||||
MAX_USERNAME_LENGTH = 14
|
||||
|
||||
def __init__(self, db_service: DatabaseService) -> None:
|
||||
self._db_service = db_service
|
||||
|
||||
async def get_all_users(self) -> list[User]:
|
||||
return await self._db_service.get_all_users()
|
||||
|
||||
async def get_user(self, accessor: Optional[Union[str, int]]) -> Optional[User]:
|
||||
if accessor is None:
|
||||
return
|
||||
if isinstance(accessor, int):
|
||||
return await self._db_service.get_user_by_id(accessor)
|
||||
accessor = accessor.lower()
|
||||
if "@" in accessor:
|
||||
return await self._db_service.get_user_by_mail(accessor)
|
||||
return await self._db_service.get_user_by_name(accessor)
|
||||
|
||||
async def set_profile_picture(self, user_id: int, picture: bytes) -> None:
|
||||
await self._db_service.set_user_profile_picture(user_id, picture)
|
||||
|
||||
async def remove_profile_picture(self, user_id: int) -> None:
|
||||
await self._db_service.remove_profile_picture(user_id)
|
||||
|
||||
async def get_profile_picture(self, user_id: int) -> bytes:
|
||||
return await self._db_service.get_user_profile_picture(user_id)
|
||||
|
||||
async def create_user(self, user_name: str, user_mail: str, password_clear_text: str) -> User:
|
||||
disallowed_char = self._check_for_disallowed_char(user_name)
|
||||
if disallowed_char:
|
||||
raise NameNotAllowedError(disallowed_char)
|
||||
|
||||
user_name = user_name.lower()
|
||||
|
||||
hashed_pw = sha256(password_clear_text.encode(encoding="utf-8")).hexdigest()
|
||||
created_user = await self._db_service.create_user(user_name, user_mail, hashed_pw)
|
||||
return created_user
|
||||
|
||||
async def update_user(self, user: User) -> User:
|
||||
disallowed_char = self._check_for_disallowed_char(user.user_name)
|
||||
if disallowed_char:
|
||||
raise NameNotAllowedError(disallowed_char)
|
||||
user.user_name = user.user_name.lower()
|
||||
return await self._db_service.update_user(user)
|
||||
|
||||
async def is_login_valid(self, user_name_or_mail: str, password_clear_text: str) -> bool:
|
||||
user = await self.get_user(user_name_or_mail)
|
||||
if not user:
|
||||
return False
|
||||
return user.user_password == sha256(password_clear_text.encode(encoding="utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _check_for_disallowed_char(self, name: str) -> Optional[str]:
|
||||
for c in name:
|
||||
if c not in self.ALLOWED_USER_NAME_SYMBOLS:
|
||||
return c
|
||||
@@ -0,0 +1,29 @@
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal
|
||||
from enum import StrEnum
|
||||
from typing import Self
|
||||
|
||||
|
||||
class CateringMenuItemCategory(StrEnum):
|
||||
MAIN_COURSE = "MAIN_COURSE"
|
||||
DESSERT = "DESSERT"
|
||||
BEVERAGE_NON_ALCOHOLIC = "BEVERAGE_NON_ALCOHOLIC"
|
||||
BEVERAGE_ALCOHOLIC = "BEVERAGE_ALCOHOLIC"
|
||||
BEVERAGE_COCKTAIL = "BEVERAGE_COCKTAIL"
|
||||
BEVERAGE_SHOT = "BEVERAGE_SHOT"
|
||||
BREAKFAST = "BREAKFAST"
|
||||
SNACK = "SNACK"
|
||||
NON_FOOD = "NON_FOOD"
|
||||
|
||||
|
||||
@dataclass(frozen=False)
|
||||
class CateringMenuItem:
|
||||
item_id: int
|
||||
name: str
|
||||
price: Decimal
|
||||
category: CateringMenuItemCategory
|
||||
additional_info: str = str()
|
||||
is_disabled: bool = False
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(str(self.item_id) + self.name)
|
||||
@@ -0,0 +1,53 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from enum import StrEnum
|
||||
from typing import Optional, Iterable, Self
|
||||
|
||||
from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItem, CateringMenuItemCategory
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
CateringMenuItemsWithAmount = dict[CateringMenuItem, int]
|
||||
|
||||
|
||||
class CateringOrderStatus(StrEnum):
|
||||
RECEIVED = "RECEIVED"
|
||||
DELAYED = "DELAYED"
|
||||
READY_FOR_PICKUP = "READY_FOR_PICKUP"
|
||||
EN_ROUTE = "EN_ROUTE"
|
||||
COMPLETED = "COMPLETED"
|
||||
CANCELED = "CANCELED"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CateringOrder:
|
||||
order_id: int
|
||||
order_date: datetime
|
||||
status: CateringOrderStatus
|
||||
items: CateringMenuItemsWithAmount
|
||||
customer: User
|
||||
is_delivery: bool = True
|
||||
|
||||
@property
|
||||
def price(self) -> Decimal:
|
||||
total = Decimal("0")
|
||||
for item, amount in self.items.items():
|
||||
total += (item.price * amount)
|
||||
return total
|
||||
|
||||
@staticmethod
|
||||
def translate_order_status(status: CateringOrderStatus) -> str:
|
||||
if status == CateringOrderStatus.RECEIVED:
|
||||
return "Eingegangen"
|
||||
elif status == CateringOrderStatus.DELAYED:
|
||||
return "Verzögert"
|
||||
elif status == CateringOrderStatus.READY_FOR_PICKUP:
|
||||
return "Abholbereit"
|
||||
elif status == CateringOrderStatus.EN_ROUTE:
|
||||
return "In Zustellung"
|
||||
elif status == CateringOrderStatus.COMPLETED:
|
||||
return "Abgeschlossen"
|
||||
elif status == CateringOrderStatus.CANCELED:
|
||||
return "Storniert"
|
||||
else:
|
||||
raise RuntimeError("Unknown CateringOrderStatus:", status)
|
||||
@@ -0,0 +1,57 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class NoSuchCategoryError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DatabaseConfiguration:
|
||||
db_user: str
|
||||
db_password: str
|
||||
db_host: str
|
||||
db_port: int
|
||||
db_name: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TicketInfo:
|
||||
category: str
|
||||
total_tickets: int
|
||||
price: Decimal
|
||||
description: str
|
||||
additional_info: str
|
||||
is_default: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MailingServiceConfiguration:
|
||||
smtp_server: str
|
||||
smtp_port: int
|
||||
sender: str
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LanInfo:
|
||||
name: str
|
||||
iteration: str
|
||||
date_from: datetime
|
||||
date_till: datetime
|
||||
organizer_mail: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SeatingConfiguration:
|
||||
seats: dict[str, str]
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReceiptPrintingConfiguration:
|
||||
host: str
|
||||
port: int
|
||||
order_print_endpoint: str
|
||||
password: str
|
||||
@@ -0,0 +1,15 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import date
|
||||
from typing import Optional
|
||||
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class News:
|
||||
news_id: Optional[int]
|
||||
title: str
|
||||
subtitle: str
|
||||
content: str
|
||||
author: User
|
||||
news_date: date
|
||||
@@ -0,0 +1,12 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Seat:
|
||||
seat_id: str
|
||||
is_blocked: bool
|
||||
category: str
|
||||
user: Optional[User]
|
||||
@@ -0,0 +1,36 @@
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
|
||||
# ToDo: Persist between reloads: https://rio.dev/docs/howto/persistent-settings
|
||||
# Note for ToDo: rio.UserSettings are saved LOCALLY, do not just read a user_id here!
|
||||
@dataclass(frozen=False)
|
||||
class SessionStorage:
|
||||
_user_id: Optional[int] = None # DEBUG: Put user ID here to skip login
|
||||
_is_team_member: bool = False
|
||||
_notification_callbacks: dict[str, Callable] = field(default_factory=dict)
|
||||
|
||||
async def clear(self) -> None:
|
||||
await self.set_user_id_and_team_member_flag(None, False)
|
||||
|
||||
def subscribe_to_logged_in_or_out_event(self, component_id: str, callback: Callable) -> None:
|
||||
self._notification_callbacks[component_id] = callback
|
||||
|
||||
@property
|
||||
def user_id(self) -> Optional[int]:
|
||||
return self._user_id
|
||||
|
||||
@property
|
||||
def is_team_member(self) -> bool:
|
||||
return self._is_team_member
|
||||
|
||||
async def set_user_id_and_team_member_flag(self, user_id: Optional[int], is_team_member: bool) -> None:
|
||||
self._user_id = user_id
|
||||
self._is_team_member = is_team_member
|
||||
for component_id, callback in self._notification_callbacks.items():
|
||||
logger.debug(f"Calling logged in callback from {component_id}")
|
||||
await callback()
|
||||
@@ -0,0 +1,13 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Ticket:
|
||||
ticket_id: int
|
||||
category: str
|
||||
purchase_date: datetime
|
||||
owner: Optional[User] = None
|
||||
@@ -0,0 +1,12 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Transaction:
|
||||
user_id: int
|
||||
value: Decimal
|
||||
is_debit: bool
|
||||
reference: str
|
||||
transaction_date: datetime
|
||||
@@ -0,0 +1,22 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
user_id: int
|
||||
user_name: str
|
||||
user_mail: str
|
||||
user_password: str
|
||||
user_first_name: Optional[str]
|
||||
user_last_name: Optional[str]
|
||||
user_birth_day: Optional[date]
|
||||
is_active: bool
|
||||
is_team_member: bool
|
||||
is_admin: bool
|
||||
created_at: datetime
|
||||
last_updated_at: datetime
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(f"{self.user_id}{self.user_name}{self.user_mail}")
|
||||
Reference in New Issue
Block a user